All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[Unreleased]

[0.8.3] - 2026-04-14

Fixed

  • Line total rounding error when discounts produce half-penny values
    • ubl_line_total/1 now rounds only the final result (qty × price × (1 − discount/100)) instead of rounding the intermediate allowance
    • allowance_charge_xml/1 derives the allowance as base_amount − line_total to preserve the BaseAmount = AllowanceAmount + LineExtensionAmount invariant

[0.8.2] - 2026-04-09

Added

  • OrderLineReference support on individual invoice and credit note lines
    • New optional order_line_reference field on line details with line_id (required) and order_reference (optional)
    • Generates <cac:OrderLineReference> with <cbc:LineID> and optional nested <cac:OrderReference>
    • Parser extracts OrderLineReference data from incoming UBL documents

[0.8.1] - 2026-02-27

Added

  • SEPA direct debit support (PaymentMeansCode 59) with PaymentMandate in invoice and credit note generators
    • When payment_means_code is "59", generates <cac:PaymentMandate> with mandate reference and debtor IBAN instead of <cac:PayeeFinancialAccount>
    • New input fields: mandate_id and debtor_iban

[0.8.0] - 2026-02-23

Changed

  • BREAKING: Attachment parsing now returns separate :id and :filename fields
    • Previously: <cbc:ID> from AdditionalDocumentReference was stored as :filename
    • Now: <cbc:ID> is stored as :id, and the filename attribute from EmbeddedDocumentBinaryObject is stored as :filename
    • If no filename attribute exists, :filename falls back to the :id value
    • Migration: Code accessing attachment.filename to get the document reference ID should use attachment.id instead
  • Generator now uses :id for the <cbc:ID> element, falling back to :filename when no :id is provided

[0.7.9] - 2026-02-21

Fixed

  • IBAN overwritten by BIC/SWIFT code when FinancialInstitutionBranch is present inside PayeeFinancialAccount
    • Previously: <cbc:ID> inside <cac:FinancialInstitutionBranch> matched the IBAN extraction rule because in_path? only checked for PayeeFinancialAccount anywhere in the path
    • Now: Excludes FinancialInstitutionBranch from the match so only the direct <cbc:ID> under PayeeFinancialAccount is captured as the IBAN

[0.7.8] - 2026-02-17

Fixed

  • Item name incorrectly overwritten by AdditionalItemProperty name during parsing
    • Previously: <cbc:Name> inside <cac:AdditionalItemProperty> matched the item name rule because in_path? found "Item" anywhere in the XML path ancestry
    • Now: Uses match?/2 to ensure Name is a direct child of Item, ignoring nested elements like AdditionalItemProperty
    • Example: An invoice line with item name "IPHONE 17 BLACK 256GB" and an AdditionalItemProperty named "TaxAmount" would incorrectly parse the item name as "TaxAmount"

[0.7.7] - 2026-01-25

Changed

  • Made optional fields truly optional per Peppol BIS 3.0 specification:
    • Supplier: street, city, zipcode, vat, email are now optional
    • Customer: street, housenumber, city, zipcode, vat are now optional
    • Only endpoint_id, name, and country are required for parties

[0.7.6] - 2026-01-15

Added

  • tax_subtotals field in parsed results exposing VAT breakdown per rate
    • Each subtotal includes percentage, taxable_amount, and tax_amount
    • Useful for invoices with multiple VAT rates

[0.7.5] - 2026-01-09

Added

  • UblEx.strip_sbdh/1 function to remove StandardBusinessDocument/StandardBusinessDocumentHeader wrapper from UBL XML
    • Useful for accounting software that cannot process SBDH-wrapped documents
    • Extracts the inner UBL document (Invoice, CreditNote, or ApplicationResponse)
    • Returns unchanged XML if no SBDH wrapper is present
    • Parsed results are identical whether SBDH is present or stripped

[0.7.4] - 2025-12-30

Added

  • Support for cbc:Note elements in invoices and credit notes per Peppol BIS 3.0:
    • Document-level note (0..1) - Optional note at invoice/credit note root level
    • Line-level note (0..1) - Optional note on individual invoice/credit note lines
    • Payment terms (0..1) - Optional cac:PaymentTerms with required cbc:Note describing payment conditions
  • Generator support for all three note types
  • Parser support for extracting all three note types
  • Full round-trip support for notes (parse → generate → parse)

[0.7.3] - 2025-12-29

Fixed

  • VAT rounding error when multiple line items share the same VAT rate
    • Previously: VAT calculated per line, then summed (e.g., 14.89 + 14.89 + 14.89 = 44.67)
    • Now: Lines grouped by VAT rate, summed, then VAT calculated once (e.g., (70.92 + 70.92 + 70.92) × 21% = 44.68)
    • Fixes 1 cent discrepancies in grand totals caused by cumulative rounding errors
    • Affects both ubl_totals/1 and tax_totals/1 functions in UblEx.Generator.Helpers

[0.7.2] - 2025-12-29

Added

  • Parser now extracts monetary totals from UBL documents, eliminating need for users to recalculate:
    • tax_amount - Total tax/VAT amount from TaxTotal element
    • line_extension_amount - Sum of all line totals before tax
    • tax_exclusive_amount - Total amount before tax (after allowances/charges)
    • tax_inclusive_amount - Total amount including tax
    • payable_amount - Final amount to be paid
    • allowance_total_amount - Total discounts/allowances (optional)
    • charge_total_amount - Total additional charges (optional)
    • prepaid_amount - Amount already paid (optional)
    • All monetary values returned as Decimal types for precision

Changed

  • Updated credo from 1.7.13 to 1.7.15
  • Updated ex_doc from 0.39.1 to 0.39.3

[0.7.1] - 2025-12-15

Fixed

  • Division by zero error when generating invoices with 100% discount
    • Previously: allowance_charge_xml/1 tried to reverse-calculate base amount from discounted total, causing crash with 100% discount (0 / 0)
    • Now: Base amount calculated directly as quantity × price, which works for any discount percentage including 100%

[0.7.0] - 2025-11-25

Changed

  • BREAKING: Tax exemption fields now required for exempt/export/intra-community/reverse charge transactions per Peppol BIS 3.0 validation rules (BR-O-11 through BR-O-14)
    • Previously: Tax categories E, G, K, AE generated without exemption reason fields
    • Now: Must provide tax_exemption_reason_code and tax_exemption_reason for these categories
    • Migration: Add VATEX codes and reasons to line items with tax_category :exempt, :export, :intra_community, or :reverse_charge
    • Example: tax_exemption_reason_code: "vatex-eu-ic", tax_exemption_reason: "Intra-community supply - Article 138 Directive 2006/112/EC"
    • Not required for: Standard VAT (S), zero-rated (Z), or outside scope (O) categories

Added

  • Tax exemption field support on line items:
    • tax_exemption_reason_code - VATEX code (e.g., "vatex-eu-ic", "vatex-eu-ae", "vatex-eu-g")
    • tax_exemption_reason - Human-readable explanation
  • Generator now includes exemption fields in TaxSubtotal sections when provided
  • Parser extracts exemption fields from TaxSubtotal and applies them to matching line items
  • Full round-trip support for exemption fields
  • Documentation with link to official Peppol VATEX code list
  • Comprehensive test coverage for all exemption scenarios

[0.6.1] - 2025-11-24

Fixed

  • Customer VAT number now preserved as-is instead of being reconstructed from customer country
    • Previously: A Swiss customer with Belgian VAT BE0123456749 would incorrectly become CH0123456749
    • Now: The original VAT number is used directly in the PartyTaxScheme/CompanyID element

Added

  • Automatic Peppol scheme inference from country code via Helpers.infer_scheme/1 and Helpers.party_scheme/1
    • If scheme is not explicitly set, it is now inferred from the party's country field
    • Supports all EU member states with their primary Peppol scheme IDs
    • Falls back to "0088" (EAN/GLN) for unmapped countries

Changed

  • External validation tests (peppol.helger.com) are now excluded by default to avoid rate limiting
    • Run with mix test --include external to include validation tests

[0.6.0] - 2025-11-24

Changed

  • BREAKING: Replaced reverse_charge boolean with tax_category atom for full Peppol BIS 3.0 tax category support
    • Previously: reverse_charge: true triggered tax category "K" (intra-community)
    • Now: Use tax_category: :intra_community for EU cross-border B2B transactions
    • Migration: Replace reverse_charge: true with tax_category: :intra_community
    • Migration: Remove reverse_charge: false (no replacement needed, :standard is the default)

Added

  • Full tax category support with descriptive atoms:
    • :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
  • Automatic inference: If tax_category not specified, defaults to :standard for non-zero VAT and :zero_rated for 0% VAT
  • Parser now converts Peppol codes back to descriptive atoms for round-trip fidelity

[0.5.0] - 2025-11-17

Changed

  • BREAKING: reverse_charge is now a per-line setting instead of document-wide
    • Previously: reverse_charge was set at the document level and applied to all line items
    • Now: reverse_charge must be set on individual line items in the details array
    • Migration: Move reverse_charge: true from document root to each applicable line item
    • Benefit: Allows mixing regular and reverse charge line items in the same invoice
    • Default: Line items without reverse_charge field default to false
    • Parser: Now extracts reverse_charge per line based on TaxCategory ID ("K" = reverse charge)

[0.4.0] - 2025-10-17

Added

  • Optional UBL document validation against Peppol BIS Billing 3.0 rules
  • UblEx.Validator.validate/3 function for validating invoices and credit notes
  • Integration with free peppol.helger.com validation web service
  • Support for custom validation options (timeout, VESID override)
  • Detailed error and warning reporting from validation service
  • Optional req dependency for validation feature (not required for core functionality)

[0.3.1] - 2025-10-17

Fixed

  • Fixed PaymentMeansCode roundtrip preservation - parser now captures original code, generator uses parsed value or intelligently defaults based on IBAN presence
  • Fixed PaymentID roundtrip - parser now extracts PaymentID from PaymentMeans element
  • Fixed IBAN roundtrip - parser now extracts and stores supplier IBAN in supplier data
  • Fixed billing reference ID duplication - removed duplicate "F" prefix in generator template
  • Fixed document ID preservation - parser now keeps full ID (e.g., "V01/F2025158") without stripping prefix, generator uses ID as-is

[0.3.0] - 2025-10-17

Added

  • Configurable customer endpoint scheme via customer.scheme field (defaults to "0208")
  • Explicit customer endpoint ID via customer.endpoint_id field (defaults to VAT number for backward compatibility)
  • Support for different Peppol identifier schemes (e.g., "9925" for organization numbers, country-specific schemes)

[0.2.0] - 2025-10-17

Changed

  • BREAKING: Migrated XML parsing from SweetXML to Saxy for improved performance
    • Direct SAX event handler eliminates XPath queries and intermediate DOM trees
    • Significantly faster parsing on large documents
  • BREAKING: Removed parse_xml/2 function - use parse/1 instead
  • BREAKING: Removed schema registry system (register_schema/2, list_schemas/0, validate_xml/2)
    • Parser now has hardcoded UBL structure knowledge instead of configurable schemas
  • Simplified API to just 3 main functions: parse/1, generate/1, generate_with_sbdh/1

Removed

  • SweetXML dependency (replaced with Saxy)
  • Old XPath-based parser infrastructure (1,185+ lines of code)
  • Schema configuration system (no longer needed with direct handler)

Fixed

  • Invalid XML now returns {:error, reason} tuple instead of raising/exiting
  • ApplicationResponse documents now properly parse :id, :sender, and :receiver fields
  • SenderParty/ReceiverParty elements handled correctly (with or without nested Party elements)
  • BREAKING: Credit notes no longer include :expires field (only invoices have DueDate per Peppol BIS Billing 3.0 spec)

Performance

  • 1,414x faster XML parsing on production-sized documents (890KB invoice with attachment)
  • 90% memory reduction compared to SweetXML in production workloads
  • 1,000 invoices/day: 37 minutes → 1.6 seconds of CPU time

[0.1.0] - 2025-10-15

Added

  • UBL document generation for Invoices, Credit Notes, and Application Responses
  • Peppol BIS Billing 3.0 compliance
  • SBDH (Standard Business Document Header) generation for Peppol network transmission
  • Full round-trip support for all document types (parse → generate → parse)
  • Automatic SBDH parsing and unwrapping
  • Attachment support for invoices and credit notes
  • Auto-detection of document types (Invoice, CreditNote, ApplicationResponse)
  • Namespace-agnostic XML parser with configurable schemas
  • Schema registry for multiple XML formats
  • Customer endpoint_id derivation from VAT for SBDH when not provided