Facturx.Validate (Facturx v0.4.0)

Copy Markdown View Source

Optional Schematron validation against the EN 16931 business rules.

The EN 16931 Schematron compiles to XSLT 2.0, which the BEAM cannot run. Like the Python akretion/factur-x library, we delegate to a Saxon server over HTTP (req — declared optional so non-validating callers don't pull an HTTP client). The compiled schematron XSLT ships in priv/schematron/.

Run a Saxon server (e.g. ghcr.io/willemvlh/saxon-server, port 5000) and point :endpoint at its /transform route. The transform is sent the XML and the XSLT as a multipart/form-data body and returns an SVRL report, which we interpret into failed-assert / successful-report findings.

Findings are split by SVRL severity: only those flagged "warning" or "info" are non-blocking, so a document carrying nothing worse stays valid.

{:ok, :valid}
{:ok, {:valid_with_warnings, [%{flag: "warning", message: "…"}]}}
{:error, {:invalid, [%{flag: nil, message: "…"}]}}

Only three assertions in the bundled schematron are flagged warning, and two are business rules rather than cosmetics: PEPPOL-EN16931-R008 (no empty elements), BR-29 (BT-74 must be ≥ BT-73) and BR-FX-EN-04 (a non-down-payment invoice must carry BT-72, BG-14 or BG-26). So {:ok, {:valid_with_warnings, _}} is not necessarily benign — inspect the findings.

The common trigger is R008: with neither :ship_to nor :delivery_date set, Facturx.CII still has to emit an empty ram:ApplicableHeaderTradeDelivery, which CII requires. Supply delivery data (which BR-FX-EN-04 wants anyway) and a plain {:ok, :valid} is the normal outcome.

Privacy: a public Saxon endpoint means sending real invoice data to a third party. Self-host the Saxon server in production.

{:ok, :valid} = Facturx.validate(xml, profile: :en16931,
                                 endpoint: "http://localhost:5000/transform")

Only the :en16931 schematron ships today; other profiles return {:error, {:schematron_not_bundled, profile}}.

Summary

Types

A schematron finding.

Functions

Validate xml against the EN 16931 Schematron via a Saxon endpoint.

Types

violation()

@type violation() :: %{
  message: String.t() | nil,
  location: String.t() | nil,
  test: String.t() | nil,
  flag: String.t() | nil
}

A schematron finding.

:flag is the SVRL severity when the rule declares one. Only findings flagged "warning" or "info" are treated as non-blocking; everything else — including an absent flag — counts as an error.

Functions

validate(xml, opts \\ [])

@spec validate(
  binary(),
  keyword()
) ::
  {:ok, :valid}
  | {:ok, {:valid_with_warnings, [violation()]}}
  | {:error, {:invalid, [violation()]}}
  | {:error, term()}

Validate xml against the EN 16931 Schematron via a Saxon endpoint.

Options:

  • :endpoint — Saxon /transform URL (defaults to the app env config :facturx, Facturx.Validate, endpoint: ..., then "http://localhost:5000/transform")
  • :profile — schematron level (defaults to the profile detected in the XML)
  • :codedb_url — override the code-list DB URL the XSLT resolves
  • :receive_timeout — HTTP receive timeout (ms), default 20_000