formalis

package module
v0.3.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 31, 2026 License: MIT Imports: 14 Imported by: 0

README

formalis

A dependency-free (Go standard library only) validator for electronic invoices.

It checks EN 16931 and the Core Invoice Usage Specifications (CIUS) layered on it, and it also checks the national invoice formats that are not EN 16931 profiles at all — Italian FatturaPA, Spanish Facturae, Austrian ebInterface, Polish KSeF, Finnish Finvoice and TEAPPS, Danish OIOUBL, Swedish Svefaktura, Hungarian NAV Online Számla, Turkish UBL-TR, Saudi ZATCA, and Peppol PINT for AE / AUNZ / EU / JP / MY / OM / SG. The Franco-German Order-X order document is validated too. Twenty-one rule sets in all; the table below names each one, the entry point that runs it, and the Source that scopes its findings.

One syntax-neutral rule engine validates both EN 16931 syntaxes — UN/CEFACT Cross Industry Invoice (CII, used by Factur-X/ZUGFeRD) and OASIS UBL (Peppol BIS, XRechnung, NLCIUS) — and each CIUS adds its own rule layer on top of it. The formats that are not EN 16931 profiles are checked against their own mandatory structure and code lists instead.

Every exported function is safe to call from any number of goroutines at once: the package holds no mutable global state, and TestValidatorsAreSafeForConcurrentUse pins that under -race.

Usage

report, err := formalis.ValidateCIUS(ctx, xml)
if err != nil {
    // The input could not be read at all: malformed XML, or an encoding this
    // package does not implement. Nothing was judged.
    return err
}

for _, v := range report.Fatal() {
    fmt.Printf("%s %s: %s\n", v.Source, v.Rule, v.Message)
}

fmt.Println("nothing found:", len(report.Violations) == 0)
fmt.Println("conformant:   ", report.Conformant())

ValidateCIUS routes the document against whichever rule set it declares. Every exported validator has this shape — func(context.Context, []byte) (Report, error) — and Report is the whole answer:

type Report struct {
    Violations   []Violation  // what was found
    NotEvaluated []RuleFamily // the rule families that were not evaluated
}

func (r Report) Conformant() bool  // no fatal finding, no closeable fatal gap, run not cut short
func (r Report) Complete() bool    // every rule anyone *can* evaluate was evaluated
func (r Report) Fatal() []Violation
func (r Report) Warnings() []Violation

The zero Report is deliberately neither Conformant nor Complete, so a value nobody filled in — including the one returned beside an error — cannot pass for a clean invoice.

Four answers, and the difference between them

The distinctions this package exists for are all in how a call can end:

Outcome Means Read it with
error, zero Report the input could not be read at all — malformed XML, an unimplemented encoding. A statement about the file errors.Is(err, formalis.ErrMalformedXML), ErrUnsupportedEncoding
a RuleLimit finding the run stopped before it had seen everything — cancelled context, tripped budget. A statement about the run formalis.IsCheckerViolation(v)
findings the document departs from rules that were evaluated report.Fatal(), report.Warnings()
no findings everything that was evaluated passed report.NotEvaluated, report.Conformant()

A run that stopped stays a finding rather than becoming an error, deliberately: the checks that did complete are still true, and an error would discard them. A well-formed document that is simply not an invoice is a finding too — RuleRoot for the EN 16931 entry points, FPA-root, ZA-root, ORDER-root … for the national ones — because the document was read.

Severity

Every Violation carries the severity its authority gave the rule: CEN's flag="fatal" or flag="warning" in the Schematron, and the national authorities' equivalents. Every one of those flags is a quotation checked against the artefact that publishes it — for CEN, KoSIT and OpenPEPPOL, and since make cius-schematron for the Portuguese, Romanian, Belgian, Serbian and Dutch rule sets as well, whose severities used to be this package's fail-safe reading of prose. SeverityFatal is the zero value, so an unstamped finding reads as blocking rather than as advisory — the fail-safe direction.

Four rule sets report warnings, and everything else this package implements is fatal:

  • the advisory halves of CEN's two EN 16931 syntax bindings — 676 UBL-CR-*, 21 UBL-DT-*, 440 CII-SR-* and 31 CII-DT-* assertions, generated from CEN's Schematron into a table this package evaluates. Their job is to hold a document down to the EN 16931 core subset of UBL and CII, which is a thing a reference validator reports and no authority rejects an invoice for;
  • the advisory "not recommended" tier of both NLCIUS bindings — BR-NL-19 to BR-NL-35, twenty identifiers in the UBL binding and twenty-one in the CII one, which SimplerInvoicing flags warning — plus the empty-element rule each binding ends with, SI-UBL-2 and empty-element-check, also flagged warning;
  • eleven of XRechnung's fifty-seven — the invoice type code, the specification identifier, the two IBAN checks, the telephone and email formats and five more, which KoSIT flags warning or information;
  • six of OpenPEPPOL's — the Italian, Danish and Swedish participant-identifier format checks PEPPOL-COMMON-R044/R045/R046/R047/R052/R053 — plus PEPPOL-EN16931-R120, which OpenPEPPOL flags fatal and KoSIT re-flags warning when it merges it into XRechnung, so the same rule is a non-conformance on the Peppol path and a warning on the German one;
  • eighteen of OpenPEPPOL's 101 country-specific rules, among them all six Swedish Bankgiro/Plusgiro checks SE-R-007..012, the Greek GR-S-008-1 and GR-S-011, the Danish DK-R-003/DK-R-017, the Norwegian NO-R-002, the Icelandic IS-R-001 and six German ones.

(This section said "one rule set and one only" until the second and third arrived, and "three" until the Dutch advisory tier landed; the KoSIT half had been true for a release before anyone corrected the sentence.)

That is checked rather than assumed, in both directions: one test sweeps the whole corpus and fails on any finding whose severity does not match the half of the package it came from, and another reads the flag off each of the EN 16931 rules the corpus exercises straight from the vendored CEN Schematron and compares it with the severity the finding carried.

Where the identifier was minted here rather than quoted — FPA-*, ZA-*, ORDER-* and the rest — there is no flag to quote, and fatal is a decision: each of those rules checks a mandatory element of the format's own schema or a value outside its own code list, so a document that breaks one is a document the authority's gateway rejects. go doc formalis.Severity says so.

Coverage's families carry the authority's flag too, and that is what makes Conformant() answerable at all: an advisory gap leaves the verdict intact and only makes the report less informative than a reference validator's, while a fatal gap means a rule that could have rejected this document was never run.

They also carry Unevaluable, which is a different fact and not a softer severity. It means the authority published a rule nobody can evaluate — CEN binds BR-CO-05..08 to the XPath expression true(), so the assertion cannot fail, and CII-DT-010/011/012 sit behind an earlier matching rule in CEN's own ISO Schematron pattern, so no processor reaches them. That second shape turns out to be common: three CIUS-RO rules, fifteen of SRBDT's thirty-six RSR-* and four NLCIUS assertions are unreachable for exactly that reason, and eight of the Serbian ones were being reported here until the rule order was read. A rule nothing can check is not a rule this package skipped, so those do not cost a verdict and do not make a run incomplete. The field is deliberately narrow: it does not mean hard, low value, or not yet, and go doc formalis.RuleFamily spells out the boundary and the two tests that hold the table to it.

What a clean report does and does not mean

Conformant() returns false for most documents, whatever they contain. That is not a bug and it is the first thing to understand about this package. The EN 16931 core, Factur-X and the seven CIUS layered on it are the exceptions today; every national format validator here returns false for a clean invoice.

len(report.Violations) == 0 means only the checks that ran found nothing. It is equally true of a run that checked everything, a run that was cancelled or hit a resource budget, and a run whose rule set does not implement every rule its authority publishes. Every rule set here is a documented subset, and Coverage(src) is where each one says so.

Conformant() is the weaker and more useful question, because it passes over the gaps an authority would not reject a document for. Nine rule sets have no fatal gap left — the core, Factur-X and every CIUS:

  • the EN 16931 core: every fatal rule of the semantic model, of the UBL binding and of the CII binding is evaluated, bar the few CEN's own reference implementation cannot report, so ValidateEN16931 — and ValidateCIUS on a document that declares no CIUS — returns Conformant() == true for a clean invoice;

  • Factur-X / ZUGFeRD: everything the five profile Schematrons publish that is fatal — the four CEN-minted binding assertions they carry, the nine BR-FXEXT-* rules that are Factur-X's own new ground, the 24 BR-FXEXT-* that restate a CEN identifier the profile drops, and all 2,159 assertions of the per-profile element data model. Validate returns Conformant() == true for a clean document at every tier, and does so for all 32 profile-declaring documents FNFE ships;

  • XRechnung: the Schematron a German buyer validates against is 78 identifiers, KoSIT's own 57 plus 21 it merges in from Peppol BIS Billing 3.0, and all 78 are evaluated. The imported findings carry SourcePeppol, because Source names the authority that wrote the rule;

  • Peppol BIS Billing 3.0: both rule sets of the two vendored OpenPEPPOL Schematron files are evaluated — the 59 PEPPOL-COMMON-* and PEPPOL-EN16931-* identifiers, and the 101 country-specific rules published in the same files under a comment reading "National rules" (DE-R-*, DK-R-*, GR-R-*/GR-S-*, IS-R-*, IT-R-*, NL-R-*, NO-R-*, SE-R-*). That is 244 (identifier, binding) pairs, each evaluated in the binding that publishes it, and every one of them has a document in the suite that trips it and one that does not. The country rules are gated the way OpenPEPPOL gates them — on the supplier's country and, for the domestic ones, the customer's — so a French invoice answers to none of them.

  • CIUS-PT: all 363 identifiers AT/eSPap publishes are evaluated — the 65 BR-CIUS-PT-* business rules, AT's own eight BR-AA-* for the "Lower rate" VAT category, and the 290 DT-CIUS-PT-* datatype and arithmetic rules over the 291 assertions that carry them. That last family is four fifths of the Portuguese rule set by count and no coverage entry here named it until the Schematron was vendored; it is generated from that Schematron rather than transcribed, the way CEN's advisory binding rules are. It is the first CIUS whose datatype tier is implemented at all.

  • CIUS-RO: all 121 identifiers ANAF publishes in release 1.0.9 — the 25 BR-RO-NNN business rules by hand, and 90 length, decimal, date-format and occurrence rules generated from RO16931-rules.sch. The six that no Schematron processor can report are marked Unevaluable;

  • UBL.BE: all 15 ubl-BE-* identifiers of the ubl-model-BE pattern, including the two on the cac:AdditionalDocumentReference group and the two bilingual free-text code lists. ubl-BE-13 is Unevaluable: the authority binds it to abs($TaxAmount) >= 0 over a variable that falls back to -1, so it cannot fail;

  • SRBDT: all 46 identifiers the Serbian Ministry of Finance publishes — 21 reachable RSR-* business rules, the 3 RSE-* srbdtext extension rules, and the 7 assertions of the abstract pdvcat pattern, which the validation schema instantiates once per zero-rate VAT category. Fifteen RSR-* are Unevaluable: EN16931-UBL-srbdt.sch is a single pattern in which eleven rules repeat the context /ubl:Invoice | /cn:CreditNote and four more repeat three other contexts, and ISO Schematron gives a node to the first matching rule of a pattern only;

  • NLCIUS: every identifier of both bindings — 43 in UBL, 34 in CII — in both halves: the twelve fatal rules, the twenty-two "not recommended" ones, which are reported as warnings, the eight BR-GA-* of the G-account extension, which the UBL binding alone publishes, and the empty-element rule each binding ends with, which each names differently (SI-UBL-2 in UBL, empty-element-check in CII). Four are Unevaluable for the rule-order reason above, and one of those was a live false positive: BR-NL-9 has a rule of its own in the CII binding, against a context BR-NL-7's rule already holds, so no CII document can be reported for it. This package reported it until the file was read that way. Nothing else is a gap — Coverage(SourceNLCIUS) holds Unevaluable entries only.

    The empty-element rule is the one rule in either binding whose context carries no gate, so ValidateNLCIUS reports it for a document that does not declare the NLCIUS specification identifier at all, which is what SimplerInvoicing's own validator does. It is also last in its pattern, so an empty element an earlier rule of that pattern already claims is reported under that rule's identifier and not as an empty element: an empty cbc:TaxCurrencyCode in a Dutch NLCIUS invoice is BR-NL-19, not SI-UBL-2.

    The G-account extension is the invoice form for a Dutch g-rekening: a blocked account into which a contractor pays the payroll-tax share of a subcontractor's invoice. Such an invoice carries two payment instructions and two payment terms rather than one of each, and the eight rules are about that split. It is opt-in — the document declares …#conformant#urn:fdc:nen.nl:gaccount:v1.0 — and it is part of NLCIUS rather than a profile beside it, because both NLCIUS binding files fold the extension's identifier into their own $si and $s gates and the extension's Schematron <include>s the whole of SI-UBL 2.0. ValidateNLCIUS applies it to a UBL Invoice that declares the extension or carries a GACCOUNT payment instruction, and to nothing else: SimplerInvoicing publishes no CII binding of it and none of its rules for a credit note.

Every national format validator still names a gap its authority flags fatal and a validator could close, so those return false whatever the document.

Complete() is the stricter question — "did this package see everything a reference validator could see" — and all eight of those rule sets answer yes. The EN 16931 core's 1,168 advisory binding rules used to be the reason it could not; they are evaluated now, and what is left in Coverage(SourceEN16931) is seven rules CEN itself cannot evaluate: four bound to the XPath expression true(), three unreachable in CEN's own Schematron rule ordering, and one whose UBL test a correctly PCI-masked card number trips. Those are marked Unevaluable, so they no longer hold the answer down — a rule nobody can check is not a rule this package skipped. The thirteen national format validators still name gaps they could close, so Complete() is false there.

The XRechnung path answers yes for a different reason: it had exactly one gap, and it was a rule set it imports rather than one of its own — the twenty-one Peppol rules the released artefact merges in, one of which (PEPPOL-EN16931-R061) had replaced KoSIT's withdrawn BR-DE-29, so BG-19's mandate reference was checked by nothing on the German path at all.

The CIUS-PT path answers yes for a fourth reason, and it is the one this package had been carrying longest: its only gap was 290 fatal rules that had never been named anywhere. ValidateCIUSPT reported Conformant() == false for every Portuguese invoice, whatever it contained, until they were implemented.

The NLCIUS path answers yes for a fifth reason, and it is the one worth reading. Implementing its "not recommended" tier made it the first rule set here whose last gap was advisory, and Complete() was true for a Dutch invoice on that basis — but the survey behind that had never counted the whole rule set. Every guard over these five national rule sets asked its question about the identifiers a prefix admitted, ^BR-NL- in this case, and the two bindings publish three things it does not match: the eight BR-GA-* of the G-account extension, and one advisory rule each binding names differently, SI-UBL-2 and empty-element-check. Naming the last of those made Complete() false again; evaluating it has made it true, and this time over an inventory that was enumerated rather than pattern-matched.

The general defect is fixed rather than the instance. Every identifier a vendored national Schematron publishes is now enumerated and then classified — its authority's own, another vendored authority's by lookup, or a named withdrawn one — and one that no classifier accounts for fails the build. A prefix could only ever enumerate what its author anticipated, which is how AT/eSPap's eight BR-AA-* rules went unnoticed before it and SimplerInvoicing's eight BR-GA-* after.

The Peppol path answers yes for a third: its only gap was a rule set nobody had counted. Both binding files hold a second family of 101 country-specific rules beside the 59 PEPPOL-* ones, and every coverage survey here had matched on the prefix PEPPOL- and stopped — so ValidatePeppol reported Conformant() == false for every document, for that reason alone.

Note what Complete() is not: it says nothing about what was found. A document with twenty fatal findings can be Complete — every rule ran, and twenty of them failed. Conformant() is the verdict; Complete() is the statement about the checker.

Rather than hide that behind a number in this file that would drift as rules land, the package makes it machine-readable:

  • Coverage(src Source) []RuleFamily names the rule families src publishes and this package does not evaluate, with the authority's flag on each and whether anyone could evaluate it at all. It takes no document, parses nothing and cannot fail, so you can ask before deciding to trust a validator.
  • Report.NotEvaluated is the same information for the run that just happened — the union across every authority that call applied. A validator that layers a CIUS on the core reports both sets.
  • Report.Complete() is false when a rule a validator could have evaluated went unevaluated, whatever its severity, or when the run stopped early. Conformant() passes over the advisory holes as well.
for _, gap := range formalis.Coverage(formalis.SourceFatturaPA) {
    fmt.Printf("not evaluated: %s [%s] unevaluable=%t\n", gap.Rules, gap.Severity, gap.Unevaluable)
}
// not evaluated: the SdI FatturaPA XSD and the SdI's consistency checks [fatal] unevaluable=false

for _, gap := range formalis.Coverage(formalis.SourceNLCIUS) {
    fmt.Printf("not evaluated: %s [%s] unevaluable=%t\n", gap.Rules, gap.Severity, gap.Unevaluable)
}
// not evaluated: BR-NL-9, in the CII binding only [fatal] unevaluable=true
// not evaluated: BR-NL-31, in the CII binding only [warning] unevaluable=true
// not evaluated: BR-NL-32-2, BR-NL-32-3, in the UBL binding only [warning] unevaluable=true

The answers are two kinds and the two lists show one each. The Italian entry is work: a rule the SdI would reject a document over, which this package does not run, so it costs Conformant(). Every Dutch entry is unevaluable=true — a fact about somebody else's file, assertions SimplerInvoicing publishes that no validator, its own included, ever reaches — so they cost neither Conformant() nor Complete(). That is why an Italian invoice with no findings is reported neither conformant nor complete and a Dutch one with no findings is reported both.

Use Conformant() when you need the strong claim, and len(r.Fatal()) == 0 when the weaker one will do — with r.NotEvaluated beside it saying exactly what it omits.

Coverage

Format Entry point Is* predicate Source
EN 16931 core (CII + UBL) ValidateEN16931, ValidateCIUS SourceEN16931
Factur-X / ZUGFeRD (FR/DE), five profiles Validate (with a Profile), ValidateCIUS SourceFacturX
XRechnung (DE) ValidateXRechnung SourceXRechnung
Peppol BIS Billing 3.0 ValidatePeppol SourcePeppol
NLCIUS / SimplerInvoicing (NL) ValidateNLCIUS SourceNLCIUS
CIUS-PT (PT) ValidateCIUSPT SourceCIUSPT
CIUS-RO / RO e-Factura (RO) ValidateCIUSRO SourceCIUSRO
UBL.BE (BE) ValidateUBLBE SourceUBLBE
SRBDT (RS) ValidateSRBDT SourceSRBDT
Peppol PINT (AE/AUNZ/EU/JP/MY/OM/SG) ValidatePINT IsPINT SourcePINT
FatturaPA / FatturaElettronica (IT) ValidateFatturaPA IsFatturaPA SourceFatturaPA
Facturae (ES) ValidateFacturae IsFacturae SourceFacturae
ebInterface (AT) ValidateEbInterface IsEbInterface SourceEbInterface
KSeF FA (PL) ValidateKSeF IsKSeF SourceKSeF
Finvoice (FI) ValidateFinvoice IsFinvoice SourceFinvoice
TEAPPSXML (FI) ValidateTEAPPS IsTEAPPS SourceTEAPPS
OIOUBL (DK) ValidateOIOUBL IsOIOUBL SourceOIOUBL
Svefaktura (SE) ValidateSvefaktura IsSvefaktura SourceSvefaktura
ZATCA (SA) ValidateZATCA IsZATCA SourceZATCA
NAV Online Számla / OSA (HU) ValidateOSA IsOSA SourceOSA
UBL-TR e-Fatura (TR) ValidateTurkishInvoice IsTurkishInvoice SourceUBLTR
Order-X (order, not invoice) ValidateOrderXML SourceOrderX

Eight of these are CIUS constants — XRechnung, Peppol, NLCIUS, CIUS-PT, CIUS-RO, UBL.BE, SRBDT and PINT — and ValidateCIUS routes to them on the document's Specification identifier (BT-24, what DetectCIUS reads). It routes to OIOUBL and UBL-TR on that same identifier though neither is a CIUS, to ZATCA on a profile identifier or a document reference, to ebInterface and Svefaktura on a distinguishing child element, to the remaining formats on their root element, and otherwise to the EN 16931 core. There is no Is* predicate for the EN 16931 core, for the seven CIUS layered on it, or for Order-X, and none is needed: the CIUS are told apart by that identifier (DetectCIUS, or Detection.CIUS), and Order-X by a root element no other format uses.

SourceChecker is a Source but not a format: it carries this package's statements about its own run, or about the file it was handed — RuleLimit, RuleProfile and RuleRoot — and Coverage(SourceChecker) is nil because it publishes no rules. SourceNone is the zero value, which Detect reports for a document it read and recognised as no format here, and which no Violation ever carries.

Routing

Detect is the routing entry point. It reads the document once, without building a tree, and arbitrates between the formats in an order that is part of its documented contract:

det, err := formalis.Detect(xml)
switch {
case err != nil:
    // Could not read it. Do not dispatch on this.
case !det.Recognised():
    // Read it; no format this package validates.
default:
    report, err := det.Validator()(ctx, xml)
}

Detection also carries SpecID (BT-24 as the document wrote it, trimmed), CIUS and Root, and Coverage(det.Source) answers what the validator it named will not check — before the call.

The twelve Is* predicates remain, and each answers about one format. They are independent tests, not a partition: more than one can be true of the same bytes (an <Invoice> with both a Biller and a SellerParty is IsEbInterface and IsSvefaktura). That is why Detect exists — it is the arbitration, written down once. Each predicate answers three ways:

ok, err := formalis.IsFacturae(xml)
switch {
case err != nil:
    // Malformed XML, an unsupported encoding, or a tripped guard.
    // Not the same thing as "not a Facturae invoice".
case ok:
    report, err := formalis.ValidateFacturae(ctx, xml)
}

Profile is a Factur-X/ZUGFeRD data-richness tier (MINIMUM, BASIC WL, BASIC, EN 16931, EXTENDED). It never selects a national rule set, and it does select Factur-X's: naming one says "judge this as Factur-X", which means the EN 16931 core, the rules that tier is expected to satisfy, and the CII syntax binding FNFE-MPE publishes for that profile rather than CEN's. ProfileFor maps a PDF's XMP ConformanceLevel to one; CIUSFor maps the levels that name a CIUS instead (today, "XRECHNUNG"). A Profile this package does not implement is refused with a RuleProfile violation rather than silently read as EN 16931.

ValidateCIUS and Detect read the tier back out of BT-24 for a document that did not come with a Profile, and they accept both identifiers each tier is published under: Factur-X 1.0 and ZUGFeRD 2.x are one specification under two brands, and FNFE's own code database enumerates urn:factur-x.eu:1p0:minimum and urn:zugferd.de:2p0:minimum for the same tier. Only the EN 16931 tier has a single value — CEN's own urn:cen.eu:en16931:2017, which is the document claiming to be exactly EN 16931 and is deliberately not routed to Factur-X.

Factur-X binds EN 16931 with its own rule set and does not adopt CEN's CII syntax binding: its five profile Schematrons carry four of CEN's 583 CII-SR-*/CII-DT-* assertions and, in their place, a per-profile data model of between 48 and 1,241 assertions of their own — 2,159 across the five tiers, one per element of that tier's element table. Judging a Factur-X document by CEN's binding reported 76 fatal findings on 13 of FNFE-MPE's own 59 published examples — documents FNFE's own validator passes. So Validate is the Factur-X verdict, and ValidateEN16931 is CEN's, with CEN's binding and no Profile because CEN publishes none.

All 2,159 of those assertions are evaluated, the 366 code-list lookups included. None of them carries an identifier in the artefact — FNFE names a rule by an [ID]- prefix on its message and these have none — so each is reported under a key this package mints, FX-DM-<PROFILE>-<NNNN>, documented in facturx_datamodel.go.

The 42 BR-FXEXT-* rules that restate a CEN identifier the EXTENDED profile drops were the last fatal gap, and the 24 of them FNFE leaves unflagged — which within these files means fatal — are evaluated now. They were expected to be duplicates of a stricter CEN rule this package already applies, and more than half of them are not: BR-FXEXT-CO-12 adds the logistics service charges (BT-X-272) EXTENDED introduces to the charge total, BR-FXEXT-CO-16 adds the charges collected on behalf of a third party (BT-179), the nine -08b summations restrict the base to DETAIL sub-lines and tighten CEN's ±1 tolerance to 0,01 × the operand count, and BR-FXEXT-CO-15 closes an escape clause CEN leaves open. facturx_restatements.go states, rule by rule, what CEN asserts, what FNFE asserts, and which fires where the other cannot.

They also run the other way, and that is a verdict rather than a list: for 21 of the 24, FNFE's reading can be satisfied on a document CEN's rule reports — a logistics charge folded into BT-108, a third-party charge folded into BT-115, a GROUP line CEN counts and FNFE does not, a cent inside a tolerance FNFE has and CEN has not. Two of FNFE's own published examples are in that position. Where a document is being validated as Factur-X, the authority that governs it decides, so the CEN finding is dropped when its Factur-X replacement holds — and only then. facturXAuthorityParity implements it, TestAuthoritySamplesDrawNoFatalFinding states the principle over every authority in the tree that ships its own conformant samples, and ValidateEN16931 is unaffected: a caller who wants CEN's own verdict on a Factur-X document still gets it, findings included.

So Validate reports Conformant() for a clean document again, at every tier. Coverage(SourceFacturX) names what is left, and none of it is fatal and evaluable: CII-SR-464, which FNFE rewrote into a tautology; three data-model assertions that contradict their own context; one OpenPEPPOL rule FNFE merges in; and the eighteen -08ini/-08rev readings FNFE flags warning, which keep Complete() false and are an advisory gap — exactly the distinction the two methods exist to draw.

One consequence is worth knowing before you count findings: this package reports the union of CEN's rule set and Factur-X's, so a document that breaks both BR-S-08 and BR-FXEXT-S08b is reported twice, once under each authority's identifier. FNFE's own validator prints one line, because its EXTENDED Schematron does not carry BR-S-08 at all. Source is the discriminator; filter on it for exactly FNFE's verdict.

Rule identity is (Source, Rule)

Source names the authority that defines a rule; two authorities may mint the same string. Aggregate and suppress on the pair, never on Rule alone:

type key struct {
    Source formalis.Source
    Rule   string
}
counts := map[key]int{}
for _, v := range report.Violations {
    counts[key{v.Source, v.Rule}]++
}

An identifier that looks like CEN's may not be CEN's. SourceCIUSPT carries BR-AA-01BR-AA-07 and BR-AA-10, eight rules AT/eSPap wrote for the "Lower rate" (AA) VAT category by cloning CEN's BR-S-* template — for a category code EN 16931 leaves out of BT-118's restricted list, so CEN publishes no BR-AA-* family at all. Keying on Rule alone would file them under the standard.

Most national formats publish no rule identifier this package could quote, so the identifiers under those Sources — FPA-*, FE-*, ZA-*, ORDER-*, … — were minted here. The Source is still the format the document was judged against, which is what a caller routing or suppressing by format needs; it is not a claim that the format's own documentation uses these names. The Sources whose identifiers are quoted from a published rule set are EN 16931, XRechnung, Peppol, NLCIUS, CIUS-PT, CIUS-RO, UBL.BE, SRBDT and Factur-X. BR-GA-* is SimplerInvoicing's too — the G-account extension is published in the same repository under the same customization identifier as NLCIUS, so its findings carry SourceNLCIUS.

A CIUS may re-write a CEN rule's condition

Some national CIUS ship a copy of CEN's Schematron rather than referencing it, and a copy can be edited. Where it has been, the authority's own validator evaluates its edited condition — so a BR-S-02 reported under that CIUS is CEN's identifier judged by that authority's reading. This package honours that, and says so:

report, _ := formalis.ValidateCIUSPT(ctx, xml)
for _, v := range report.Violations {
    if v.Reading != formalis.SourceNone {
        // v.Rule is CEN's, v.Source is SourceEN16931, and v.Reading names the
        // authority whose condition decided it. This finding will not reproduce
        // under a plain EN 16931 validation, and that is correct.
    }
}

Violation.Reading is SourceNone on every other finding, and Violation.Error renders it, so a caller who only logs findings can still see it.

Which conditions count as the authority's own is derived rather than judged. A copy that differs from CEN's current file usually differs because CEN changed the file afterwards, which is a stale directory and not a national rule; the generator in testdata/cius-condition-overrides/ separates the two by asking CEN's own git history whether CEN ever published what the copy carries. The result today:

Authority CEN identifiers in its copy Same as CEN's current file A CEN condition from an earlier release The authority's own
CIUS-PT 2.1.1 (UBL) 771 27 735 9, applied
CIUS-RO 1.0.9 (UBL) 930 904 26 0
NLCIUS SI-UBL 2.0.3.2 929 866 63 0
NLCIUS G-account 1.0.2 745 700 45 0 — but see below
NLCIUS 1.0.3 (CII) 733 628 105 0
UBL.BE v1.31 250 205 38 7, recorded and not applied
SRBDT 1.0.0 ships no copy of CEN's files

The nine Portuguese ones are the VAT category aliases NORS and ISEE across BR-S-02/03/04/10 and BR-E-02/03/04/10, and BR-23, which AT/eSPap inverts from an assertion into a report. Only ValidateCIUSPT applies them, only to UBL documents, and only to those nine identifiers.

A copy can also remove a rule, which is an axis the table above does not have: it compares the condition of each identifier a copy carries, and an assertion that is not carried has no condition to compare. One copy removes a rule by commenting it out. The G-account extension's EN16931-syntax-modified.sch comments out UBL-CR-411, UBL-CR-453 and UBL-CR-459 — "a UBL invoice should not include the PaymentMeans ID / the PaymentTerms PaymentMeansID / the PaymentTerms Amount" — because those three elements are precisely what the extension carries its payment split in. All three are advisory, and ValidateNLCIUS suppresses them for a document inside the extension and for no other. The set is derived by comparing the modified file against the unmodified one in the same release directory, which is what makes it a measurement of SimplerInvoicing's edit rather than of the gap between two release dates.

What a copy leaves out

A copy can also simply not carry a rule, and absence has the same two causes as a differing condition. The authority left the rule out, or CEN had not written it yet. Telling them apart needs the release the copy was taken from, and that is derived from the copy's own content: the CEN release that publishes every identifier the copy carries and whose assertions the copy reproduces most closely. Version strings are not consulted — a CIUS says "EN 16931" in its title whichever release it copied.

Authority CEN release it vendored Identifiers CEN had published and it left out Identifiers CEN has added since
CIUS-PT 2.1.1 (UBL) validation-1.1.0 (2018-06-26) 114 78
CIUS-RO 1.0.9 (UBL) validation-1.3.8 (2022-04-08) 0 26
NLCIUS SI-UBL 2.0.3.2 validation-1.3.6 (2021-05-30) 0 28
NLCIUS 1.0.3 (CII) validation-1.3.11.3.4 (2020-02-25) 0 50

A release range means CEN republished those files unchanged and the evidence cannot pin them any finer, which is said rather than rounded away. Three copies are not in the table and each says why in ciusCENCopyOmissions: SRBDT ships no copy of CEN's files at all, the G-account extension <include>s the whole of SI-UBL 2.0 so its omissions are that row's, and UBL.BE's merged file re-cases 671 of CEN's identifiers (UBL-CR-001 as ubl-CR-001), which is a question about its identifier namespace rather than about absence.

CIUS-PT is the only one that leaves a CEN rule out at all. What it leaves out is the whole BR-CL-* code-list tier — its master Schematron includes no code-list file of any name, where CIUS-RO's and both NLCIUS masters include CEN's — the BR-AE-*, BR-G-*, BR-IC-*, BR-O-* and BR-Z-* VAT category families, BR-CO-09..17, BR-DEC-*, and a handful more.

None of them is suppressed, and that is a decision rather than an omission. Suppressing a rule the authority dropped is only right when the authority put something in its place: AT/eSPap did, for the arithmetic tier — DT-CIUS-PT-160..167 answer the same questions as BR-CO-10..17 — but with a ±1.00 € acceptance range where CEN's are exact identities. Across this repository's corpus the Portuguese rule stays silent on 10 UBL documents where the CEN rule it displaced fires, so honouring it instead would leave those documents reported by nothing. For the code-list tier and the five VAT category families there is no Portuguese counterpart at all. Honouring the deletion would turn a divergence from AT's validator into a class of invoice nothing checks, and a false negative is worse than a false positive because nothing reports it.

The consequence a caller should know: ValidateCIUSPT reports fatal EN 16931 findings on all 20 instances AT/eSPap publishes as conformant — 216 of them — and every one is under an identifier AT's own rule set does not contain. Not one is this package over-reporting a rule AT publishes. If you need "what AT's validator would say", filter the report against Coverage and the identifiers named in ciusCENCopyOmissions; if you need "is this invoice EN 16931-conformant and CIUS-PT-conformant", which is what ValidateCIUSPT answers, take it as it comes.

Bounded work

Validation honours ctx and is bounded by the package's own limits on document depth and element count, so the cost of a call is not set by a hostile document. A run that stops early — a cancelled context, a tripped budget — reports a RuleLimit violation rather than an empty Violations slice, so it can never be read as a clean invoice:

for _, v := range report.Violations {
    if formalis.IsCheckerViolation(v) {
        // The checker did not judge this document.
        // Neither conformant nor non-conformant: unknown.
    }
}

RuleRoot is deliberately not a checker violation: "this document is not an EN 16931 invoice" is a definite answer about a document that was read. Neither is the error an unreadable document produces, which does not arrive as a finding at all.

Examples

Runnable versions of everything above live in example_test.go as Go Example functions, written against the exported API from outside the package. go test compiles them and checks their output, so an example that has drifted from the API fails the build rather than misleading a reader. go doc shows them alongside the symbols they document.

Tests

make test runs the suite. Without the reference corpora it exercises the rule engine against hand-written fixtures and skips the oracle-backed tests; that is the mode a clean checkout and the fast CI job run in, and it is green.

The oracles need corpora this repository does not vendor (testdata/ is gitignored, with one carve-out: the six lean-tier Factur-X invoices under testdata/facturx/extracted/ are committed, because they exist only inside PDF/A-3 containers and no fetch target can produce them — that directory's README says why, and facturx_lean_tiers_test.go holds them as an expected-failure table: what each draws, at which node, and a written reason that is itself checked against the document and the artefact). Fetching the rest needs git, bash, curl, python3 — several Romanian and Portuguese sample filenames are non-ASCII and are URL-encoded with it — and gh authenticated against GitHub, because the fetch makes about fifteen gh api calls and the unauthenticated rate limit of 60 requests an hour is not enough for them. make check-deps verifies all of this up front and names what is missing, rather than failing two hundred lines into a download.

make check-deps                    # what the fetch targets need
make cius-oracles                  # ~600 documents: XRechnung, Peppol, NLCIUS,
                                   # the CIUS and the national-format samples,
                                   # plus (via cius-schematron) the five national
                                   # Schematrons the severity and coverage guards
                                   # read
make cius-schematron               # those Schematrons on their own: CIUS-PT,
                                   # CIUS-RO, UBL.BE, SRBDT and NLCIUS, plus each
                                   # authority's own copy of CEN's files
make en16931-artefacts             # the CEN/TC 434 per-rule unit-test suite, cloned
                                   # with full history (see condition overrides)
make en16931-ubl                   # the EN 16931 UBL example invoices
make en16931-genericode            # the official code lists (needs unzip)
make en16931-syntax-rules          # regenerate the advisory binding table
make cius-pt-rules                 # regenerate the CIUS-PT datatype table
make cius-ro-rules                 # regenerate the CIUS-RO length/decimal table
make cius-condition-overrides      # regenerate the per-CIUS condition-override table
make test

Each target is stamped, so re-running it is a no-op rather than an error; the matching clean-* target removes the stamp and the data, and is how you force a re-fetch.

Five targets go one step further and regenerate committed source. make en16931-codelists rewrites the code-list tables from the genericode bundle, make en16931-syntax-rules rewrites the advisory syntax-binding table from the CEN Schematron, make cius-pt-rules rewrites the CIUS-PT datatype table from AT/eSPap's, make cius-ro-rules rewrites the CIUS-RO length, decimal, date-format and occurrence table from ANAF's, and make cius-condition-overrides rewrites the table of CEN conditions each CIUS re-wrote — derived from every authority's copy of CEN's Schematron and from CEN's own git history, which is why make en16931-artefacts clones full depth. All five are deliberate acts and none is part of running the tests, which is why fetching each oracle is its own target — and in both cases a test re-derives the same data from the same source on every run and fails if the committed table has drifted. The generators refuse to write anything they cannot describe rather than skipping it: a rule quietly dropped by a generator is a rule that silently stops being checked, with nothing to notice.

With the corpora present the suite runs with no skips, and each oracle ratchets the number of documents it saw — the constants are collected in corpus_test.go. A corpus that arrives truncated therefore fails the build rather than reporting a clean verdict over whatever landed, and the fetch itself fails on the first download it cannot complete.

Documentation

Overview

Package formalis validates electronic invoices against the EN 16931 semantic model, the Core Invoice Usage Specifications (CIUS) layered on top of it, and the national invoice formats that stand outside EN 16931 altogether. It uses nothing but the Go standard library.

One syntax-neutral rule engine (fed by parseEN16931) serves both EN 16931 syntaxes — UN/CEFACT Cross Industry Invoice (CII, used by Factur-X/ZUGFeRD) and OASIS UBL (Peppol BIS, XRechnung UBL, NLCIUS) — and each CIUS adds its own rule layer in its own file (xrechnung.go, peppol.go, nlcius.go, cius_pt.go, cius_ro.go, cius_be.go, cius_rs.go). The formats that are not EN 16931 profiles at all — FatturaPA, Facturae, ebInterface, KSeF, Finvoice, TEAPPS, OIOUBL, Svefaktura, ZATCA, NAV OSA, UBL-TR, PINT — and the Order-X order document are checked against their own mandatory structure and code lists by validators of their own. Source names every one of these authorities, and a rule is identified by the pair (Source, Rule) rather than by Rule alone.

Entry points

Every exported validator takes a context and the document's bytes and returns a Report and an error. Which one to call:

  • Detect routes. It reports which format a document is in, in one streaming pass that builds no tree, and Detection.Validator returns the entry point that checks it.
  • ValidateCIUS applies that same arbitration and validates in one call, falling back to the EN 16931 core for a document that declares nothing.
  • Validate checks a document as Factur-X, at a named Profile: the EN 16931 core, the rules that tier is expected to satisfy, and the syntax binding FNFE-MPE publishes for it rather than CEN's. It is for a caller who has that data-richness metadata from a container the invoice itself does not carry.
  • ValidateEN16931 checks it as CEN's own EN 16931, with CEN's binding and no Factur-X rule. It takes no Profile, because CEN publishes none.
  • The format-specific validators (ValidateXRechnung, ValidateZATCA, …) are the direct route when the format is already known.

The twelve Is* predicates answer "is this document format X?" for one format each. They are independent tests and not a partition — more than one can be true of the same bytes — so route with Detect, which arbitrates between them in an order it documents.

The four answers a validator can give

The distinctions this package exists for are all in how a call can end, so they are worth reading once:

  • An error, with the zero Report: the input could not be read at all — malformed XML, an encoding this package does not implement. A statement about the file. See ErrMalformedXML and ErrUnsupportedEncoding.
  • A Report holding a RuleLimit finding: the run stopped before it had seen everything, so nothing can be concluded. A statement about the run, and never an empty Violations slice. See RuleLimit and IsCheckerViolation.
  • A Report holding findings: the document departs from rules that were evaluated. Each carries the Severity its authority gave the rule, so Report.Fatal and Report.Warnings separate "reject this" from "note this".
  • A Report holding no findings: everything that was evaluated passed. What was evaluated is the other half of the answer, and Report.NotEvaluated is where it is written down.

No rule set in this package evaluates everything its authority publishes: each is a documented subset. Coverage names the gaps for any Source, with the severity of each and whether anyone could evaluate it at all, and it takes no document, so a caller can ask what a validator will not look at before deciding to call it. Report.NotEvaluated repeats those gaps for the run that just happened; Report.Conformant is false whenever a rule that could have rejected this document was one a validator could have evaluated and this package did not, and Report.Complete whenever any evaluable rule went unevaluated. Today CEN's EN 16931 core is the one rule set with no unevaluated fatal rule and the one whose clean documents report both Conformant and Complete — reached by ValidateEN16931, or by ValidateCIUS on a document declaring no profile at all. Validate reports Conformant and not Complete, and every national format validator still names a fatal gap it could close and so reports false whatever the document.

Validate spent two releases in that last group deliberately. It reported Conformant for a clean Factur-X document while it was judging one by CEN's CII syntax binding, which Factur-X does not adopt — 76 fatal findings on 13 of FNFE-MPE's own 59 published examples — so scoping the binding correctly made the answer false until Factur-X's own rule set was evaluated in its place. It is now: the per-profile data model, between 48 and 1,241 assertions a tier, and the 33 BR-FXEXT-* rules that are fatal, nine of them Factur-X's own new ground and 24 restatements of a CEN identifier the profile drops. What is left under SourceFacturX is advisory or unevaluable, which is why Conformant is true again and Complete is not.

The distinction the third field carries is worth one sentence here, because it is what makes Complete answerable rather than permanently false: CEN publishes seven rules that no validator can evaluate — four bound to the XPath expression true(), three unreachable in CEN's own Schematron rule ordering — and a rule nobody can check is not a rule this package skipped. See RuleFamily.Unevaluable, which documents how narrow that is, Report and Coverage.

Three rule sets report warnings, and every other finding in this package is fatal. CEN flags 1,168 of its two syntax bindings' assertions warning rather than fatal — the UBL-CR-*, UBL-DT-*, CII-SR-* and CII-DT-* rules that hold a document down to the EN 16931 core subset of UBL and CII — and this package evaluates all of them from tables generated out of CEN's own Schematron. KoSIT flags eleven of XRechnung's fifty-seven rules warning or information: the invoice type code, the specification identifier, the two IBAN checks, the telephone and email formats and five more. OpenPEPPOL flags six of its fifty-nine warning — the Italian, Danish and Swedish participant-identifier format checks — and one more, PEPPOL-EN16931-R120, is fatal in OpenPEPPOL's own Schematron and warning in the XRechnung artefact that merges it, so the same rule is a non-conformance on one path and advisory on the other. None of these is a verdict: a document whose only findings are these is Conformant, and a caller gating on Report.Fatal never sees them. See Severity and Report.Warnings.

Concurrency

There is no global mutable state. The code-list tables, the compiled regexps and the generated syntax-binding tables (parsed once at load by en16931_syntax_advisory.go) are package-level values initialised once at load and only read afterwards, and every per-call artefact — the run, the parsed tree, the semantic model — is allocated inside the call. Every exported function may therefore be called from any number of goroutines at once; TestValidatorsAreSafeForConcurrentUse pins that under -race.

A note on pdf0, which this package's comments refer to

This package reads XML and has no PDF dependency. pdf0 (github.com/mgilbir/pdf0) is a separate, public sibling module that wraps it for the Factur-X container: pdf0 opens the PDF, extracts the attached invoice XML, and calls in here. The dependency runs one way — pdf0 requires formalis, and nothing in this module imports or needs pdf0 — so the references to it in limits.go, orderx.go and facturx_en16931.go are design constraints, not dependencies. They record conventions the two modules deliberately share, RuleLimit above all, so that a caller draining one mixed slice of container and invoice findings has one name to look for rather than two. A reader with no interest in PDF containers can disregard every one of them; nothing here changes behaviour because of pdf0.

Example

Example validates a document against whichever rule set it declares, and reads the answer the way this package intends: not "were there findings?" but "was anything left unexamined?".

The error is a separate question from the findings. It means the input could not be read at all — malformed XML, an encoding this package does not implement — and nothing about the invoice, which is why it is not a finding.

The four lines it prints are four different facts, and the last two are the ones worth reading together. This document declares no CIUS, so it is validated against the EN 16931 core: nothing was found; the core has no rule left that CEN would reject this invoice over and that this package did not check, so it is conformant; and it is complete, because everything left unevaluated is a rule CEN published that no validator can evaluate — four bound to the XPath expression true(), three unreachable in CEN's own Schematron rule ordering, one whose test a correctly masked card number trips.

So "gaps" and "complete" are both true at once, and that is not a contradiction: Report.NotEvaluated still names those seven rules, because a caller comparing this package against a reference validator deserves to know they exist, while Report.Complete passes over them because no reference validator evaluates them either. RuleFamily.Unevaluable is the field that separates the two.

A document declaring a CIUS whose fatal rules are only partly implemented prints conformant: false and complete: false, with the missing families in Report.NotEvaluated.

package main

import (
	"context"
	"fmt"

	"github.com/mgilbir/formalis"
)

// exampleUBL is a minimal EN 16931 UBL invoice declaring no CIUS.
const exampleUBL = `<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<CustomizationID>urn:cen.eu:en16931:2017</CustomizationID>
<ID>INV-1</ID><IssueDate>2024-01-15</IssueDate>
<InvoiceTypeCode>380</InvoiceTypeCode><DocumentCurrencyCode>EUR</DocumentCurrencyCode>
<AccountingSupplierParty><Party>
  <PostalAddress><Country><IdentificationCode>DE</IdentificationCode></Country></PostalAddress>
  <PartyTaxScheme><CompanyID>DE123456789</CompanyID><TaxScheme><ID>VAT</ID></TaxScheme></PartyTaxScheme>
  <PartyLegalEntity><RegistrationName>Seller Ltd</RegistrationName></PartyLegalEntity>
</Party></AccountingSupplierParty>
<AccountingCustomerParty><Party>
  <PostalAddress><Country><IdentificationCode>DE</IdentificationCode></Country></PostalAddress>
  <PartyLegalEntity><RegistrationName>Buyer Ltd</RegistrationName></PartyLegalEntity>
</Party></AccountingCustomerParty>
<TaxTotal><TaxAmount>19.00</TaxAmount>
  <TaxSubtotal><TaxableAmount>100.00</TaxableAmount><TaxAmount>19.00</TaxAmount>
    <TaxCategory><ID>S</ID><Percent>19</Percent></TaxCategory></TaxSubtotal>
</TaxTotal>
<LegalMonetaryTotal><LineExtensionAmount>100.00</LineExtensionAmount>
  <TaxExclusiveAmount>100.00</TaxExclusiveAmount><TaxInclusiveAmount>119.00</TaxInclusiveAmount>
  <PayableAmount>119.00</PayableAmount></LegalMonetaryTotal>
<InvoiceLine><ID>1</ID><InvoicedQuantity unitCode="C62">1</InvoicedQuantity>
  <LineExtensionAmount>100.00</LineExtensionAmount>
  <Item><Name>Widget</Name><ClassifiedTaxCategory><ID>S</ID><Percent>19</Percent></ClassifiedTaxCategory></Item>
  <Price><PriceAmount>100.00</PriceAmount></Price></InvoiceLine>
</Invoice>`

func main() {
	report, err := formalis.ValidateCIUS(context.Background(), []byte(exampleUBL))
	if err != nil {
		fmt.Println("could not read it:", err)
		return
	}

	for _, v := range report.Fatal() {
		fmt.Printf("%s %s: %s\n", v.Source, v.Rule, v.Message)
	}

	fmt.Println("nothing found:", len(report.Violations) == 0)
	fmt.Println("conformant:   ", report.Conformant())
	fmt.Println("complete:     ", report.Complete())
	fmt.Println("gaps:         ", len(report.NotEvaluated) > 0)

}
Output:
nothing found: true
conformant:    true
complete:      true
gaps:          true

Index

Examples

Constants

View Source
const RuleLimit = "limit"

RuleLimit is the rule identifier carried by a Violation that reports the checker stopping early — a cancelled context or a tripped resource budget — rather than a defect in the invoice. Such a finding carries SourceChecker, because it is a statement by this package and not by any rule authority.

It matches the identifier pdf0 uses for the same event, so a caller that already separates "the file is bad" from "the checker could not finish" needs only one name for the second.

View Source
const RuleProfile = "profile"

RuleProfile is the rule identifier carried by a Violation that reports the caller naming a Profile this package does not implement. Like RuleLimit and RuleRoot it carries SourceChecker, because it is a statement by this checker; unlike either it is a statement about the *request*, not about the document, which is innocent and was not examined.

It exists because the alternatives are all worse. Returning an error, as this package does for a document it could not read, would say the input was unreadable when the input was never looked at and the caller's argument was the problem. Reporting it as RuleRoot would accuse a document that may be perfectly conformant of being the wrong kind of document. Reporting it as RuleLimit would overload an identifier documented as a resource-budget or cancellation event and shared verbatim with pdf0, so a caller that routes "the checker ran out of room, retry it smaller" would retry forever on input no retry can fix. And returning no findings at all would be the one outcome this package refuses everywhere else: a caller testing len(v) == 0 for "valid" would get a clean bill of health from a run that never chose a rule set. So a run that rejects the profile validates nothing and returns exactly this one finding — never mixed with document findings, because there are none to mix it with.

It is a reserved word, like RuleLimit, rather than an identifier in anyone's numbering scheme, and IsCheckerViolation recognises it: "I did not judge this document" is exactly what that predicate exists to keep separate from "conformant".

View Source
const RuleRoot = "root"

RuleRoot is the rule identifier for a document this package read and that is not an EN 16931 invoice in either syntax — a root element that is neither a CrossIndustryInvoice nor a UBL Invoice or CreditNote. Unlike RuleLimit this *is* a statement about the document, but it is still this checker's statement rather than a rule authority's, so it too carries SourceChecker.

It replaces the RuleSyntax constant, which meant two things: "this file is not well-formed XML" and "this file is not an invoice document at all". The first is now an error — see ErrMalformedXML — because there is no document to make a finding about, and the second stays a finding because there is. Narrowing RuleSyntax to the surviving half would have been the quieter change and the worse one: a caller filtering on it to catch malformed files would still compile and would never match one again. Being made to look at the call site is the point, and "root" is what the surviving meaning is actually about — the same word the thirteen tree-reading validators already use for it (FPA-root, ZA-root, ORDER-root, …), whose findings are unchanged.

It is not an IsCheckerViolation, for the reason RuleSyntax was not: "you handed me a Facturae and I check EN 16931 invoices" is a definite answer, not a confession that the checker did not look.

Variables

View Source
var (
	// ErrMalformedXML reports input the XML decoder rejected: not well-formed,
	// no root element at all, an entity reference this decoder refuses. The
	// wrapped error is the decoder's own, so %v on the result names the position.
	ErrMalformedXML = errors.New("the invoice XML is not well-formed")

	// ErrUnsupportedEncoding reports a document declaring a character encoding
	// this package does not implement. UTF-8, US-ASCII, ISO-8859-1/15 and
	// Windows-1252 are read; anything else is refused rather than passed through,
	// because the bytes of a UTF-16 or EBCDIC document read as UTF-8 are not the
	// document — element names come out mangled, and the rules would report
	// business-rule violations about text the sender never wrote. The wrapped
	// detail is the encoding the document declared.
	ErrUnsupportedEncoding = errors.New("the invoice XML declares a character encoding this package does not implement")
)

The two ways this package can fail to read a document at all.

Both are statements about the file rather than about the invoice, which is why they are errors and not findings: there is no document to judge, so any finding would be a claim about something nobody read. A stopped run is the other way round — see RuleLimit — and the two are never confused.

They are sentinels so the discrimination a caller actually makes is available without parsing a message. "The sender's file is corrupt, ask for it again" and "this producer emits UTF-16, add a transcoding step" are different operational answers, and errors.Is is how a caller tells them apart. Nothing finer is offered on purpose: the decoder's own text is wrapped in and carries the line and column, which is the only thing that says *where* a document broke, and a taxonomy of XML defects on top of that would be a second vocabulary for something encoding/xml already describes well.

Functions

func DetectPINTJurisdiction added in v0.2.0

func DetectPINTJurisdiction(customizationID string) string

DetectPINTJurisdiction returns the jurisdiction code declared in a PINT CustomizationID (e.g. "eu", "ae", "jp", "my", "sg", "om", "aunz"), or "".

The pre-release Japanese identifier is answered too. JP PINT 0.1.2 wrote its jurisdiction into the path — "urn:fdc:peppol:jp:billing:3.0" — rather than into the "@jp-1" suffix the released profiles use, and a caller applying jurisdiction-specific handling to a document this package routes to PINT should not have to know which vintage it came from.

func IsCheckerViolation added in v0.2.0

func IsCheckerViolation(v Violation) bool

IsCheckerViolation reports whether v describes the checker not having judged the document, rather than a way in which the invoice departs from the rules.

A cancelled context, a tripped resource budget (both RuleLimit) and a Profile this package does not implement (RuleProfile) all produce one. Treat it as "unknown", never as "conformant" and never as "non-conformant".

RuleRoot is deliberately *not* one of them: "this document is not an EN 16931 invoice" is a finding about the document, and a definite one. Neither is the error a document this package cannot read produces, which does not arrive as a finding at all.

It tests Rule alone, deliberately, even though every finding this package emits now carries a Source and the pair a caller should think in is (SourceChecker, RuleLimit). Two reasons. Both identifiers are reserved words rather than identifiers in anyone's numbering scheme — no rule authority mints a rule called "limit" or "profile" — so there is nothing for the scope to disambiguate here. And RuleLimit is shared with pdf0, which constructs that same finding for its own container guards and hands it back in one mixed slice; requiring SourceChecker would silently reclassify every one of those as a business-rule violation the moment this package added the field. A caller that wants the strict pair can still write it — the Source is there — but the predicate that exists to keep "unknown" from being read as "conformant" must not start returning false for a finding it has always covered.

Widening it to RuleProfile is safe in the direction that mattered there: pdf0 never emits RuleProfile, so no finding that exists today changes classification. What would not be safe is the reverse — leaving it out, so that a caller filtering with this predicate to count document defects counted its own bad argument as one.

Example

ExampleIsCheckerViolation separates "the invoice is wrong" from "the checker did not judge it". A cancelled run reports a RuleLimit violation rather than an empty Violations slice, so it can never be read as a clean invoice.

package main

import (
	"context"
	"fmt"

	"github.com/mgilbir/formalis"
)

// exampleUBL is a minimal EN 16931 UBL invoice declaring no CIUS.
const exampleUBL = `<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<CustomizationID>urn:cen.eu:en16931:2017</CustomizationID>
<ID>INV-1</ID><IssueDate>2024-01-15</IssueDate>
<InvoiceTypeCode>380</InvoiceTypeCode><DocumentCurrencyCode>EUR</DocumentCurrencyCode>
<AccountingSupplierParty><Party>
  <PostalAddress><Country><IdentificationCode>DE</IdentificationCode></Country></PostalAddress>
  <PartyTaxScheme><CompanyID>DE123456789</CompanyID><TaxScheme><ID>VAT</ID></TaxScheme></PartyTaxScheme>
  <PartyLegalEntity><RegistrationName>Seller Ltd</RegistrationName></PartyLegalEntity>
</Party></AccountingSupplierParty>
<AccountingCustomerParty><Party>
  <PostalAddress><Country><IdentificationCode>DE</IdentificationCode></Country></PostalAddress>
  <PartyLegalEntity><RegistrationName>Buyer Ltd</RegistrationName></PartyLegalEntity>
</Party></AccountingCustomerParty>
<TaxTotal><TaxAmount>19.00</TaxAmount>
  <TaxSubtotal><TaxableAmount>100.00</TaxableAmount><TaxAmount>19.00</TaxAmount>
    <TaxCategory><ID>S</ID><Percent>19</Percent></TaxCategory></TaxSubtotal>
</TaxTotal>
<LegalMonetaryTotal><LineExtensionAmount>100.00</LineExtensionAmount>
  <TaxExclusiveAmount>100.00</TaxExclusiveAmount><TaxInclusiveAmount>119.00</TaxInclusiveAmount>
  <PayableAmount>119.00</PayableAmount></LegalMonetaryTotal>
<InvoiceLine><ID>1</ID><InvoicedQuantity unitCode="C62">1</InvoicedQuantity>
  <LineExtensionAmount>100.00</LineExtensionAmount>
  <Item><Name>Widget</Name><ClassifiedTaxCategory><ID>S</ID><Percent>19</Percent></ClassifiedTaxCategory></Item>
  <Price><PriceAmount>100.00</PriceAmount></Price></InvoiceLine>
</Invoice>`

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	cancel()

	report, err := formalis.ValidateCIUS(ctx, []byte(exampleUBL))
	if err != nil {
		fmt.Println("could not read it:", err)
		return
	}
	for _, v := range report.Violations {
		if formalis.IsCheckerViolation(v) {
			fmt.Printf("unknown: %s %s\n", v.Source, v.Rule)
		} else {
			fmt.Printf("defect:  %s %s\n", v.Source, v.Rule)
		}
	}
	fmt.Println("conformant:", report.Conformant())

}
Output:
unknown: checker limit
conformant: false

func IsEbInterface added in v0.2.0

func IsEbInterface(xmlData []byte) (bool, error)

IsEbInterface reports whether the XML is an ebInterface document. The root element is "Invoice" (as in UBL), so it is disambiguated by the ebInterface- specific Biller element.

A non-nil error means the document could not be read — malformed XML, an unsupported character encoding, or a guard that tripped — and the bool is meaningless. It is distinct from (false, nil), which says the document was read and is some other format.

The Is* predicates are independent tests, not a partition. This one keys on a distinguishing child of a root four national formats, seven CIUS and the EN 16931 UBL binding all share — the weakest evidence of the twelve, since no other format forbids that child — so more than one can report true about the same document: <Invoice><Biller/><SellerParty/></Invoice> satisfies this predicate and IsSvefaktura both. Detect applies a documented precedence — this one wins that pair — and returns a single answer; route with it.

func IsFacturae added in v0.2.0

func IsFacturae(xmlData []byte) (bool, error)

IsFacturae reports whether the XML is a Facturae document.

A non-nil error means the document could not be read — malformed XML, an unsupported character encoding, or a guard that tripped — and the bool is meaningless. It is distinct from (false, nil), which says the document was read and is some other format.

The Is* predicates are independent tests, not a partition: several of them key on a root element name that four national formats, seven CIUS and the EN 16931 UBL binding all share, and more than one can report true about one document. This one keys on a root no other format claims, so nothing overlaps it today. Detect owns the precedence for the whole set and returns a single answer; route with it.

Example

ExampleIsFacturae shows the three answers every Is* predicate gives. They are independent tests rather than a partition — more than one can be true of the same bytes — so use Detect to route and these to ask about one format.

package main

import (
	"fmt"

	"github.com/mgilbir/formalis"
)

// exampleUBL is a minimal EN 16931 UBL invoice declaring no CIUS.
const exampleUBL = `<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<CustomizationID>urn:cen.eu:en16931:2017</CustomizationID>
<ID>INV-1</ID><IssueDate>2024-01-15</IssueDate>
<InvoiceTypeCode>380</InvoiceTypeCode><DocumentCurrencyCode>EUR</DocumentCurrencyCode>
<AccountingSupplierParty><Party>
  <PostalAddress><Country><IdentificationCode>DE</IdentificationCode></Country></PostalAddress>
  <PartyTaxScheme><CompanyID>DE123456789</CompanyID><TaxScheme><ID>VAT</ID></TaxScheme></PartyTaxScheme>
  <PartyLegalEntity><RegistrationName>Seller Ltd</RegistrationName></PartyLegalEntity>
</Party></AccountingSupplierParty>
<AccountingCustomerParty><Party>
  <PostalAddress><Country><IdentificationCode>DE</IdentificationCode></Country></PostalAddress>
  <PartyLegalEntity><RegistrationName>Buyer Ltd</RegistrationName></PartyLegalEntity>
</Party></AccountingCustomerParty>
<TaxTotal><TaxAmount>19.00</TaxAmount>
  <TaxSubtotal><TaxableAmount>100.00</TaxableAmount><TaxAmount>19.00</TaxAmount>
    <TaxCategory><ID>S</ID><Percent>19</Percent></TaxCategory></TaxSubtotal>
</TaxTotal>
<LegalMonetaryTotal><LineExtensionAmount>100.00</LineExtensionAmount>
  <TaxExclusiveAmount>100.00</TaxExclusiveAmount><TaxInclusiveAmount>119.00</TaxInclusiveAmount>
  <PayableAmount>119.00</PayableAmount></LegalMonetaryTotal>
<InvoiceLine><ID>1</ID><InvoicedQuantity unitCode="C62">1</InvoicedQuantity>
  <LineExtensionAmount>100.00</LineExtensionAmount>
  <Item><Name>Widget</Name><ClassifiedTaxCategory><ID>S</ID><Percent>19</Percent></ClassifiedTaxCategory></Item>
  <Price><PriceAmount>100.00</PriceAmount></Price></InvoiceLine>
</Invoice>`

func main() {
	for _, doc := range []string{
		`<Facturae><FileHeader/></Facturae>`,
		exampleUBL,
		`<Facturae>`, // truncated: not well-formed
	} {
		ok, err := formalis.IsFacturae([]byte(doc))
		switch {
		case err != nil:
			fmt.Println("could not tell; do not dispatch on this")
		case ok:
			fmt.Println("Facturae")
		default:
			fmt.Println("read it; some other format")
		}
	}

}
Output:
Facturae
read it; some other format
could not tell; do not dispatch on this

func IsFatturaPA added in v0.2.0

func IsFatturaPA(xmlData []byte) (bool, error)

IsFatturaPA reports whether the XML is a FatturaElettronica document.

A non-nil error means the document could not be read — malformed XML, an unsupported character encoding, or a guard that tripped — and the bool is meaningless. It is distinct from (false, nil), which says the document was read and is some other format.

The Is* predicates are independent tests, not a partition: several of them key on a root element name that four national formats, seven CIUS and the EN 16931 UBL binding all share, and more than one can report true about one document. This one keys on a root no other format claims, so nothing overlaps it today. Detect owns the precedence for the whole set and returns a single answer; route with it.

func IsFinvoice added in v0.2.0

func IsFinvoice(xmlData []byte) (bool, error)

IsFinvoice reports whether the XML is a Finvoice document.

A non-nil error means the document could not be read — malformed XML, an unsupported character encoding, or a guard that tripped — and the bool is meaningless. It is distinct from (false, nil), which says the document was read and is some other format.

The Is* predicates are independent tests, not a partition: several of them key on a root element name that four national formats, seven CIUS and the EN 16931 UBL binding all share, and more than one can report true about one document. This one keys on a root no other format claims, so nothing overlaps it today. Detect owns the precedence for the whole set and returns a single answer; route with it.

func IsKSeF added in v0.2.0

func IsKSeF(xmlData []byte) (bool, error)

IsKSeF reports whether the XML is a KSeF Faktura document.

A non-nil error means the document could not be read — malformed XML, an unsupported character encoding, or a guard that tripped — and the bool is meaningless. It is distinct from (false, nil), which says the document was read and is some other format.

The Is* predicates are independent tests, not a partition: several of them key on a root element name that four national formats, seven CIUS and the EN 16931 UBL binding all share, and more than one can report true about one document. This one keys on a root no other format claims, so nothing overlaps it today. Detect owns the precedence for the whole set and returns a single answer; route with it.

func IsOIOUBL added in v0.2.0

func IsOIOUBL(xmlData []byte) (bool, error)

IsOIOUBL reports whether the XML is an OIOUBL Invoice.

A non-nil error means the document could not be read — malformed XML, an unsupported character encoding, or a guard that tripped — and the bool is meaningless. It is distinct from (false, nil), which says the document was read and is some other format.

The Is* predicates are independent tests, not a partition. This one reads the Specification identifier of a root four national formats, seven CIUS and the EN 16931 UBL binding all share, so more than one can report true about the same document: "TR-OIOUBL-2.02" satisfies this predicate and IsTurkishInvoice both. Detect applies a documented precedence — this one wins that pair — and returns a single answer; route with it.

The identifier test is the OIOUBL entry of specIDRules, the same one the routing reads, so the predicate and the route cannot disagree about which identifiers name OIOUBL.

func IsOSA added in v0.2.0

func IsOSA(xmlData []byte) (bool, error)

IsOSA reports whether the XML is a Hungarian OSA InvoiceData document.

A non-nil error means the document could not be read — malformed XML, an unsupported character encoding, or a guard that tripped — and the bool is meaningless. It is distinct from (false, nil), which says the document was read and is some other format.

The Is* predicates are independent tests, not a partition: several of them key on a root element name that four national formats, seven CIUS and the EN 16931 UBL binding all share, and more than one can report true about one document. This one keys on a root no other format claims, so nothing overlaps it today. Detect owns the precedence for the whole set and returns a single answer; route with it.

func IsPINT added in v0.2.0

func IsPINT(xmlData []byte) (bool, error)

IsPINT reports whether the XML is a Peppol PINT invoice or credit note.

A non-nil error means the document could not be read — malformed XML, an unsupported character encoding, or a guard that tripped — and the bool is meaningless. It is distinct from (false, nil), which says the document was read and is some other format.

The Is* predicates are independent tests, not a partition. This one reads the Specification identifier of a root four national formats, the CIUS and the EN 16931 UBL binding all share, so more than one can report true about the same document: an invoice declaring "urn:peppol:pint:x" and ProfileID "reporting:1.0" satisfies this predicate and IsZATCA both. Detect applies a documented precedence — this one wins that pair — and returns a single answer; route with it.

The identifiers it accepts are the PINT entry of specIDRules, which is also what Detect, DetectCIUS and ValidateCIUS route on, so this predicate cannot disagree with them about which documents are PINT.

func IsSvefaktura added in v0.2.0

func IsSvefaktura(xmlData []byte) (bool, error)

IsSvefaktura reports whether the XML is an SFTI Svefaktura invoice.

A non-nil error means the document could not be read — malformed XML, an unsupported character encoding, or a guard that tripped — and the bool is meaningless. It is distinct from (false, nil), which says the document was read and is some other format.

The Is* predicates are independent tests, not a partition. This one keys on a distinguishing child of a root four national formats, seven CIUS and the EN 16931 UBL binding all share — the weakest evidence of the twelve, since no other format forbids that child — so more than one can report true about the same document: <Invoice><Biller/><SellerParty/></Invoice> satisfies this predicate and IsEbInterface both. Detect applies a documented precedence — IsEbInterface wins that pair — and returns a single answer; route with it.

func IsTEAPPS added in v0.2.0

func IsTEAPPS(xmlData []byte) (bool, error)

IsTEAPPS reports whether the XML is a TEAPPS batch document.

A non-nil error means the document could not be read — malformed XML, an unsupported character encoding, or a guard that tripped — and the bool is meaningless. It is distinct from (false, nil), which says the document was read and is some other format.

The Is* predicates are independent tests, not a partition: several of them key on a root element name that four national formats, seven CIUS and the EN 16931 UBL binding all share, and more than one can report true about one document. This one keys on a root no other format claims, so nothing overlaps it today. Detect owns the precedence for the whole set and returns a single answer; route with it.

func IsTurkishInvoice added in v0.2.0

func IsTurkishInvoice(xmlData []byte) (bool, error)

IsTurkishInvoice reports whether the XML is a UBL-TR Invoice.

A non-nil error means the document could not be read — malformed XML, an unsupported character encoding, or a guard that tripped — and the bool is meaningless. It is distinct from (false, nil), which says the document was read and is some other format.

The Is* predicates are independent tests, not a partition, and this is the broadest of the twelve: any Specification identifier beginning "TR", on a root four national formats, seven CIUS and the EN 16931 UBL binding all share. It therefore reports true alongside others — "TR-OIOUBL-2.02" satisfies it and IsOIOUBL both. Detect applies a documented precedence — a brand name in the identifier beats a two-character prefix, so that document is OIOUBL — and returns a single answer; route with it.

func IsZATCA added in v0.2.0

func IsZATCA(xmlData []byte) (bool, error)

IsZATCA reports whether the XML is a ZATCA (Fatoora) UBL invoice, identified by its reporting/clearance ProfileID.

A non-nil error means the document could not be read — malformed XML, an unsupported character encoding, or a guard that tripped — and the bool is meaningless. It is distinct from (false, nil), which says the document was read and is some other format.

The Is* predicates are independent tests, not a partition. This one reads a ProfileID and a document reference rather than the Specification identifier, on a root four national formats, seven CIUS and the EN 16931 UBL binding all share, so more than one can report true about the same document: an invoice declaring "urn:peppol:pint:x" and ProfileID "reporting:1.0" satisfies this predicate and IsPINT both. Detect applies a documented precedence — BT-24 is a claim about the rule set and a profile identifier is not, so that document is PINT — and returns a single answer; route with it.

Types

type CIUS

type CIUS string

CIUS identifies a Core Invoice Usage Specification the dispatcher recognises.

const (
	CIUSNone      CIUS = ""          // plain EN 16931 core (no recognised CIUS)
	CIUSXRechnung CIUS = "XRechnung" // German public-sector CIUS
	CIUSPeppol    CIUS = "Peppol"    // OpenPEPPOL BIS Billing 3.0
	CIUSNLCIUS    CIUS = "NLCIUS"    // Dutch SimplerInvoicing / SI-UBL
	CIUSPortugal  CIUS = "CIUS-PT"   // Portuguese AT/eSPap CIUS-PT
	CIUSRomania   CIUS = "CIUS-RO"   // Romanian ANAF RO e-Factura
	CIUSBelgium   CIUS = "UBL.BE"    // Belgian UBL.BE
	CIUSSerbia    CIUS = "SRBDT"     // Serbian SRBDT

	// CIUSPINT is Peppol PINT: the global UBL 2.1 billing model with
	// jurisdiction-aligned rule sets (AE, AUNZ, EU, JP, MY, OM, SG).
	//
	// It is strictly not a CIUS of EN 16931 the way the others here are — PINT is
	// its own billing model rather than a national narrowing of the European
	// norm, and this package validates it with its own rule set, ValidatePINT.
	// It is in this type because a PINT identifier is a Specification identifier
	// like any other, and leaving it out is what made a PINT invoice answer
	// CIUSPeppol: a value that says "Peppol BIS Billing 3.0 applies to this
	// document" when it does not.
	CIUSPINT CIUS = "PINT"
)

func CIUSFor added in v0.2.0

func CIUSFor(level string) (CIUS, bool)

CIUSFor maps an XMP ConformanceLevel string to the CIUS it names, for the levels that name one rather than a Factur-X profile. It is the companion to ProfileFor over the same input, matched the same way; exactly one of the two reports true for any level either recognises.

Today that is only "XRECHNUNG" (ZUGFeRD 2.x). The CIUS it returns is the one ValidateCIUS routes on and ValidateXRechnung implements, so a caller that reaches here reaches a validator that actually applies the BR-DE-* rules.

This says what the container's metadata claims. DetectCIUS says what the invoice itself declares in BT-24, and that is the more reliable of the two: prefer it, and treat a disagreement as the container and its attachment describing different documents.

func DetectCIUS

func DetectCIUS(specID string) CIUS

DetectCIUS reports the CIUS that a Specification identifier (BT-24) declares, or CIUSNone when it names no CIUS this package recognises.

The tests are applied in the order specIDRules states, which matters wherever one profile's identifier contains another's discriminator: PINT is checked before Peppol ("urn:peppol:pint:billing-1@my-1" contains "peppol" and is not a Peppol BIS Billing 3.0 document), and XRechnung before Peppol (an XRechnung identifier may also reference the Peppol base). Matching ignores case and surrounding whitespace.

CIUSNone is not the same as "no rule set": three of the profiles the identifier can name — PINT is a CIUS here, but OIOUBL and UBL-TR are national formats rather than CIUS — so an OIOUBL identifier reports CIUSNone while Detect reports SourceOIOUBL for the same document. Route on Detection.Source; read the CIUS to know which CIUS was declared.

It takes the identifier, not the document. To ask the question of XML, call Detect: Detection.SpecID is this identifier, extracted from either syntax in one streaming pass, and Detection.CIUS is this function applied to it.

type Detection added in v0.2.0

type Detection struct {
	// Source is the arbitrated answer: the authority whose format this document
	// is in, and therefore whose rules apply to it. It is SourceNone when the
	// document was read and matched no format this package validates.
	//
	// It is Source and not a Format type of its own because the two would name
	// the same twenty-one things. Source already means "the authority that
	// defines a rule", and for every value Detect can return that authority and
	// the format are one — FatturaPA is a format and a rule set, XRechnung is a
	// CIUS and a rule set, and a UBL invoice declaring no national profile is
	// judged by CEN. Reusing it buys two things a parallel taxonomy would not:
	// Coverage(det.Source) answers "what would that validator not check?"
	// *before* the call is made, and the Source on every Violation the call
	// returns is comparable with the Source the routing was done on. The values
	// Detect never returns are SourceChecker, which is this package speaking
	// about its own run rather than a format, and SourceNone.
	Source Source

	// CIUS is the Core Invoice Usage Specification the Specification identifier
	// declares — DetectCIUS(SpecID) and nothing else. It is CIUSNone when the
	// identifier names none, and for every root that carries no BT-24 at all.
	//
	// It is not always the same answer as Source, and where the two differ
	// Source is the one to route on: CIUS answers only "which CIUS", so it is
	// CIUSNone for a document Source recognises by a national format that is not
	// a CIUS (an OIOUBL or UBL-TR identifier), by evidence outside BT-24 (a
	// ZATCA profile identifier, an ebInterface Biller), or by its root element.
	// The two no longer disagree about which rule set applies, which they did
	// while a PINT invoice reported CIUSPeppol and SourcePINT.
	CIUS CIUS

	// SpecID is the Specification identifier (BT-24) as the document wrote it,
	// with nothing removed but the surrounding whitespace: the
	// cbc:CustomizationID of a UBL Invoice or CreditNote, or the
	// ExchangedDocumentContext/GuidelineSpecifiedDocumentContextParameter/ID of
	// a CrossIndustryInvoice. It is "" for any other root, and for a document
	// that omits the term.
	//
	// This is the string DetectCIUS was always documented to take and that
	// nothing exported could produce. It is kept verbatim so a caller can log
	// it, meter it, or match it against a profile this package does not know
	// about.
	SpecID string

	// Root is the local name of the document's root element, namespace
	// discarded — the fact most of the detection rests on, kept so a caller can
	// tell an Invoice from a CreditNote and see why the answer came out as it
	// did.
	Root string
}

Detection is what Detect concluded about one document.

The zero Detection is a "recognised nothing" answer — Source is SourceNone, Recognised is false and Validator is nil — so a Detection that was never filled in cannot pass for a format.

func Detect added in v0.2.0

func Detect(xmlData []byte) (Detection, error)

Detect reports which of the formats this package validates a document is in, and which CIUS it declares, in one streaming pass that builds no tree.

It is the routing entry point, and the order it applies is part of its contract rather than an implementation detail: a caller that routes on the answer needs to know why one format won.

The three answers

None of them is folded into another:

  • a non-nil error means the document could not be read — malformed XML, an encoding this package does not implement, or nesting past the cap — and the Detection is the zero value and means nothing;
  • a Detection whose Recognised is false means the document was read and is no format this package validates;
  • otherwise Source names the format, Validator returns the entry point that checks it, and Coverage(Source) says in advance what that entry point will not look at.

Detect also answers the CIUS question on its own account: Detection.SpecID is the Specification identifier (BT-24) that DetectCIUS takes and that nothing exported could otherwise extract from XML, and Detection.CIUS is DetectCIUS applied to it.

Why an ordered answer is needed

The twelve Is* predicates are independent tests, not a partition. Six of them key on a root element named Invoice or CreditNote — a name four national formats and every EN 16931 UBL document share — and disambiguate on a child that no other format forbids, so more than one of them says true about the same bytes:

<Invoice><Biller/><SellerParty/></Invoice>                     IsEbInterface, IsSvefaktura
<Invoice><CustomizationID>TR-OIOUBL-2.02</CustomizationID>…     IsOIOUBL, IsTurkishInvoice
<Invoice><CustomizationID>urn:peppol:pint:x</CustomizationID>
        <ProfileID>reporting:1.0</ProfileID></Invoice>          IsZATCA, IsPINT

Each of those answers is individually correct — IsOIOUBL means "the specification identifier says OIOUBL", not "this is OIOUBL and nothing else" — and each is worth being able to ask on its own. What was missing is the arbitration. A caller routing a mailbox has to pick an order, the package documented none, and so every caller picked a different one and got a different answer to the same question. Detect is that order, written down once, tested, and shipped as part of the API rather than left in a README example that reads like a partition.

The order

Evidence is ranked by how much of the document it consumes and how narrowly the thing it matches identifies a format.

  1. A root element that belongs to exactly one format — Facturae, FatturaElettronica, InvoiceData, Finvoice, INVOICE_CENTER, SCRDMCCBDACIOMessageStructure, Faktura with a Naglowek, and CrossIndustryInvoice. These cannot compete with one another: a document has one root and no two of these formats claim the same name. Nothing later can overturn them, because a root name that only one vocabulary uses is the strongest evidence a document offers.

  2. Within the shared UBL root (Invoice, CreditNote), the Specification identifier — BT-24, the cbc:CustomizationID. This is the one business term whose entire purpose is to name the rule set the document follows, so it outranks every structural hint. The tests that read it are the ordered list specIDRules in cius.go, most specific first, and that list is the same one DetectCIUS, the Is* predicates and ValidateCIUS read, so no two of them can answer differently. Three entries are ordered rather than merely listed:

    a. "peppol:pint" (PINT) before the bare "peppol" (Peppol BIS Billing 3.0). "urn:peppol:pint:billing-1@my-1" is a Malaysian PINT invoice and contains the substring "peppol"; PINT and BIS Billing are different rule sets, and the identifier names PINT. The pre-release Japanese identifier "urn:fdc:peppol:jp:billing:3.0" is read as PINT too, for the reason written out at that entry.

    b. "xrechnung" before "peppol", because an XRechnung identifier may also reference the Peppol base.

    c. "OIOUBL" before the "TR" prefix. Denmark's identifier carries a brand name that appears in no other profile; the Turkish test is a two-character prefix over a namespace everyone shares, which is the weakest of the identifier tests and therefore the last of them. That is what decides the audit's "TR-OIOUBL-2.02": OIOUBL.

  3. ZATCA, which reads a ProfileID value and an AdditionalDocumentReference rather than BT-24, so it runs after everything that reads BT-24. Real ZATCA invoices carry no CustomizationID at all, so this costs nothing in practice; the contrived document that carries "urn:peppol:pint:x" and "reporting:1.0" together is PINT, because the specification identifier is a claim about the rule set and a profile identifier is not.

  4. The presence of a distinguishing child — Biller (ebInterface), then SellerParty (Svefaktura). This is the weakest evidence in the table and the pair is the weakest arbitration in it: a document carrying both is not a real document of either format, and the order exists so the answer is at least fixed and stated. ebInterface goes first because Biller belongs to ebInterface's own vocabulary and appears in no UBL schema, while SellerParty is an ordinary UBL 1.0 party role that a UBL 1.0 document other than a Svefaktura could also carry.

  5. Anything still rooted Invoice or CreditNote is reported as EN 16931. The root name is exactly what parseEN16931 dispatches the UBL mapper on, so it is real evidence rather than a shrug, and the alternative — refusing to answer — would leave the caller with nothing for the single most common document this package sees.

Everything else is SourceNone: a document that was read and recognised as no format this package validates. That is a third answer, distinct from the error, and the distinction is the same one the Is* predicates draw between (false, nil) and a non-nil error. Collapsing "I read this and it is not a format I know" into "I could not read this" would say something false about the file, and collapsing it the other way would route a truncated invoice to a validator that then reports another format's rules against it.

What it costs

One streaming pass, the same one the Is* predicates make, over the same docShape. Detect builds no tree and spends no element budget, so the document that exhausts the parser's budget is still routable — which is the point of routing before validating.

Example

ExampleDetect routes a document without a table of the caller's own. The three answers Detect can give are kept apart: an error means it could not be read, an unrecognised Detection means it was read and is no format this package validates, and anything else names the rule set and the entry point.

package main

import (
	"context"
	"fmt"

	"github.com/mgilbir/formalis"
)

func main() {
	// A Malaysian Peppol PINT invoice. Its specification identifier contains the
	// substring "peppol", and it is not a Peppol BIS Billing 3.0 document; the
	// arbitration Detect applies is what tells the two apart.
	const pint = `<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
	<CustomizationID>urn:peppol:pint:billing-1@my-1</CustomizationID>
	</Invoice>`

	det, err := formalis.Detect([]byte(pint))
	switch {
	case err != nil:
		fmt.Println("could not read it:", err)
	case !det.Recognised():
		fmt.Println("read it; no format this package validates")
	default:
		fmt.Println("root:  ", det.Root)
		fmt.Println("source:", det.Source)
		fmt.Println("CIUS:  ", det.CIUS)

		// Detection.Validator is ValidatePINT here, so the findings are PINT's
		// and not Peppol BIS Billing 3.0's.
		report, verr := det.Validator()(context.Background(), []byte(pint))
		if verr != nil {
			fmt.Println("could not read it:", verr)
			return
		}
		fmt.Println("judged by:", report.Violations[0].Source)
	}

}
Output:
root:   Invoice
source: PINT
CIUS:   PINT
judged by: PINT

func (Detection) Recognised added in v0.2.0

func (d Detection) Recognised() bool

Recognised reports whether Detect matched a format at all. It is false for the third of Detect's three answers — the document was read, and is no format this package validates.

func (Detection) Validator added in v0.2.0

func (d Detection) Validator() func(context.Context, []byte) (Report, error)

Validator returns the exported entry point that validates a document of this format, or nil when Detect recognised nothing.

It exists so that routing on a Detection needs no table of the caller's own, which is the other half of owning the precedence: an order nobody can act on without re-deriving the format-to-validator map has only moved the problem.

det, err := formalis.Detect(data)
if err != nil { ... }          // could not read it
v := det.Validator()
if v == nil { ... }            // read it, recognised nothing
report, err := v(ctx, data)

The validator's own error is for the same three inputs Detect's is — malformed XML, an encoding this package does not implement — so a caller that got a Detection has usually already ruled it out. It is returned anyway rather than dropped, because the two passes read the bytes independently and nothing guarantees the caller passed the same ones.

Two of the mappings are worth stating. SourceEN16931 maps to ValidateCIUS rather than to Validate: Detect has already established that the document declares no CIUS, which is the branch on which ValidateCIUS runs the EN 16931 core, and it avoids inventing a Profile. A caller who knows the Factur-X data-richness profile from a PDF container's XMP should call Validate with it instead, since a leaner profile is excused rules the core applies — Detect reads the invoice, and the invoice does not carry that metadata. Each CIUS maps to its own validator rather than to ValidateCIUS, which is the direct route to the same rule set: ValidateCIUS now applies this very arbitration, so either call runs what Detect named.

The returned function re-reads xmlData. Detection is a separate pass by design — it builds no tree and spends no budget, which is what lets it run on input the validator may go on to refuse.

type Profile

type Profile string

Profile is a Factur-X/ZUGFeRD conformance profile: how much of the EN 16931 semantic model a document undertakes to carry, in increasing data richness. The five values below are the whole set, and Profile means only this. It is not a rule set chosen by nationality: a Core Invoice Usage Specification is a separate concept carried by CIUS, reached through ValidateCIUS or a CIUS-specific validator such as ValidateXRechnung.

Keeping the two apart is what makes the type checkable. While XRechnung was also a Profile constant it was accepted by Validate and applied no BR-DE-* rule, so the call that looked most like "validate this as XRechnung" was the one that validated it least; and because any string was accepted, a mistyped profile was silently read as EN 16931. Validate now reports a Profile it does not implement — see RuleProfile.

A Profile selects a rule set, and not only an excuse list

Naming a Profile says "judge this document as Factur-X". Two things follow.

The first is the excuse list, which is all a Profile used to be: MINIMUM and BASIC WL are head-only, so the invoice-line rules (BR-12, BR-16) are not applied to them, and MINIMUM additionally omits the buyer postal address (BR-10, BR-11), the VAT breakdown (BR-CO-18) and the amount-due summation (BR-CO-16). TestProfilesThatDifferStillDiffer pins each of those differences.

EXTENDED used to carry a fourth entry — an exemption from the allowance and charge total summations (BR-CO-11, BR-CO-12) "whose operands it may carry unitemized" — and it is gone. Measured over the corpus, unitemized totals are a producer habit and not a property of the tier: two EXTENDED documents have them and three UBL documents do, while 23 EXTENDED documents were exempted that never needed it. Both rules now apply at every profile, and the two documents that needed the exemption get it from the authority that governs them rather than from a profile test — see facturXAuthorityParity.

The second is which syntax binding a CII document is held to, and it is why this type is no longer only an excuse list. Factur-X publishes a CII binding of its own and does not adopt CEN's: the five profile Schematrons carry four of CEN's 583 CII-SR-*/CII-DT-* assertions, and in their place a per-profile data model of their own. Applying CEN's binding to a Factur-X document applies a rule set whose purpose is to hold a document down to the EN 16931 core subset of CII to a document whose profile exists to carry more than that subset — measured over FNFE's own 59 published examples, 13 conforming EXTENDED invoices were reported with 76 fatal findings naming rules Factur-X does not impose. So a Profile now selects Factur-X's binding, and EXTENDED additionally brings in 33 BR-FXEXT-* rules that are Factur-X's own — nine that are new ground and 24 that restate a CEN identifier EXTENDED drops — while MINIMUM brings in one more, BR-FXEXT-G-08. Those are the second way the named rule sets differ between tiers; the third is that 21 of those 24 restatements *supersede* the CEN identifier they restate, so EXTENDED can be silent on a CEN rule whose Factur-X replacement is satisfied. TestBasicEN16931AndExtendedDifferOnlyInTheRulesEXTENDEDPublishes pins that the list is exhaustive. See Validate, SourceFacturX and facturx_restatements.go.

That data model is the larger half of what a Profile now decides, and it is where the five tiers differ most. It is one assertion per element of that tier's element table — 48 in MINIMUM, 196 in BASIC WL, 262 in BASIC, 412 in EN 16931, 1,241 in EXTENDED — and the answers are genuinely per tier: MINIMUM does not use the buyer postal address at all, the EN 16931 tier forbids a formatted issue date on a document reference by type code, and EXTENDED permits it. So the same document is not conformant at every tier, and it is not meant to be: a Profile names the tier a document *claims*, and the tier's element table is what that claim is worth. facturx_datamodel.go evaluates it and TestFacturXTiersDifferInTheirDataModel measures the difference.

A caller who wants CEN's own EN 16931 verdict, with CEN's binding and no Factur-X rule at all, calls ValidateEN16931, which takes no Profile because CEN publishes none.

const (
	ProfileMinimum  Profile = "MINIMUM"
	ProfileBasicWL  Profile = "BASIC WL"
	ProfileBasic    Profile = "BASIC"
	ProfileEN16931  Profile = "EN 16931"
	ProfileExtended Profile = "EXTENDED"
)

func ProfileFor

func ProfileFor(level string) (Profile, bool)

ProfileFor maps an XMP ConformanceLevel string to the Factur-X profile it names. The value is matched case- and space-insensitively.

It reports false for "XRECHNUNG", which is a level a ZUGFeRD 2.x producer really does write but which names the German CIUS rather than a data-richness profile; CIUSFor maps that one. A caller reading a PDF's XMP therefore asks both, and gets from the pair the two facts the metadata actually carries — how rich the data claims to be, and which national rule set it claims to follow — instead of one value that conflates them. Neither returning ("", false) for the level (which loses it) nor returning a Profile that no validator honours (which was the bug) would do that.

type Report added in v0.2.0

type Report struct {
	// Violations is every finding: the ways the document departs from the rules
	// that were evaluated, plus any statement by this checker about its own run
	// (RuleLimit, RuleProfile — see IsCheckerViolation) or about the file it was
	// handed (RuleRoot).
	Violations []Violation

	// NotEvaluated names the rule families the rule set that ran does not
	// implement. A validator that composes rule sets reports the union:
	// ValidateCIUSPT runs the EN 16931 core and the CIUS-PT rules, so its
	// NotEvaluated holds both sources' gaps.
	//
	// It is empty when no rule set was selected — an unknown Profile — because
	// naming the gaps of a rule set that was never chosen would say something
	// about the document, and nothing was checked. Complete is false there too,
	// through the RuleProfile finding.
	NotEvaluated []RuleFamily
	// contains filtered or unexported fields
}

Report is the outcome of one validation: what the checker found, and whether it was in a position to find everything.

The second half is the point. A bare []Violation can say "here is what is wrong" and "here is nothing", and a caller reasonably reads the second as "this invoice is fine" — but "nothing" is produced by four quite different runs: one that checked everything and found nothing; one that was cancelled; one that hit a resource budget; and one that ran a rule set which does not implement every rule its authority publishes. Complete separates the first from the other three, and Conformant is the predicate that gets it right.

The zero Report is deliberately neither Conformant nor Complete, so a Report that was never filled in — a var nobody assigned, a struct decoded from JSON that had no such field, the value returned alongside an error a caller chose to ignore — cannot pass for a clean invoice. The unexported ran field is what holds that: see its comment, because the guard is easy to lose.

func Validate

func Validate(ctx context.Context, xmlData []byte, profile Profile) (Report, error)

Validate validates an invoice XML as Factur-X: against the EN 16931 core business rules, at the named Factur-X data-richness profile, and against the syntax binding Factur-X publishes for that profile rather than CEN's.

It accepts either syntax — a UN/CEFACT Cross Industry Invoice (Factur-X/ZUGFeRD) or an OASIS UBL Invoice/CreditNote (Peppol BIS, XRechnung UBL) — detecting which from the root element and mapping it onto the shared semantic model before running the one rule engine. Factur-X is a CII format and publishes no UBL binding, so a UBL document handed to this call is judged by CEN's UBL binding, unscoped; there is nothing of Factur-X's for it to be judged by. Everything below is about the CII half.

profile does two things, and the second is new. It excuses the rules a leaner tier is not expected to satisfy — MINIMUM and BASIC WL carry no invoice lines, so the line rules are not applied to them; MINIMUM also omits the buyer address, the VAT breakdown and the amount-due summation; EXTENDED is exempt from the allowance/charge total summations. And it selects the CII syntax binding: FNFE-MPE's for that profile, which carries four of CEN's 583 CII-SR-*/CII-DT-* assertions and a profile data model of its own, plus, at EXTENDED, the BR-FXEXT-* rules Factur-X adds. Profile and SourceFacturX argue why; facturx.go is where it is done.

A caller who wants CEN's own EN 16931 verdict — CEN's binding, no Factur-X rule — calls ValidateEN16931 instead. The two coincided until CEN's binding was measured against FNFE's published examples and found to report 76 fatal findings on 13 conforming EXTENDED invoices.

What profile does *not* do is select a national rule set. It cannot make this call check XRechnung, Peppol or any other CIUS; for those use ValidateCIUS, which routes on the document's own BT-24, or the CIUS-specific validator.

A Profile this package does not implement is refused rather than assumed: the call validates nothing and returns a single RuleProfile violation, so a typo cannot be read as a clean invoice.

ctx bounds how long the call may take; the work itself is bounded by this package's own limits. A cancelled run reports a RuleLimit violation rather than an empty Violations slice, so a run that stopped early cannot be read as a clean invoice or credit note.

The error is for input that could not be read at all — XML that is not well-formed, or a character encoding this package does not implement. That is a statement about the file rather than about the invoice, and it is the one answer no Report can carry honestly, since there is no document to judge. The Report returned with it is the zero Report, so a caller who ignores the error cannot read the value as clean either. Everything else, including a well-formed document that is not an invoice, is a finding: see ErrMalformedXML and RuleRoot.

The Report names the rule families this package does not evaluate, from both Sources it declares — see Coverage(SourceEN16931) and Coverage(SourceFacturX), neither of which is empty. Neither holds a fatal evaluable gap, so Report.Conformant is true for a document with no fatal finding, which it was not while the Factur-X rule set was being landed a tier at a time. Report.Complete stays false: what is left under both Sources is advisory families and rules their own authority published and no processor can report, which is the distinction those two methods exist to draw.

Example

ExampleValidate checks the EN 16931 core against a declared Factur-X profile, and shows what a finding looks like.

package main

import (
	"context"
	"fmt"
	"strings"

	"github.com/mgilbir/formalis"
)

// exampleUBL is a minimal EN 16931 UBL invoice declaring no CIUS.
const exampleUBL = `<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<CustomizationID>urn:cen.eu:en16931:2017</CustomizationID>
<ID>INV-1</ID><IssueDate>2024-01-15</IssueDate>
<InvoiceTypeCode>380</InvoiceTypeCode><DocumentCurrencyCode>EUR</DocumentCurrencyCode>
<AccountingSupplierParty><Party>
  <PostalAddress><Country><IdentificationCode>DE</IdentificationCode></Country></PostalAddress>
  <PartyTaxScheme><CompanyID>DE123456789</CompanyID><TaxScheme><ID>VAT</ID></TaxScheme></PartyTaxScheme>
  <PartyLegalEntity><RegistrationName>Seller Ltd</RegistrationName></PartyLegalEntity>
</Party></AccountingSupplierParty>
<AccountingCustomerParty><Party>
  <PostalAddress><Country><IdentificationCode>DE</IdentificationCode></Country></PostalAddress>
  <PartyLegalEntity><RegistrationName>Buyer Ltd</RegistrationName></PartyLegalEntity>
</Party></AccountingCustomerParty>
<TaxTotal><TaxAmount>19.00</TaxAmount>
  <TaxSubtotal><TaxableAmount>100.00</TaxableAmount><TaxAmount>19.00</TaxAmount>
    <TaxCategory><ID>S</ID><Percent>19</Percent></TaxCategory></TaxSubtotal>
</TaxTotal>
<LegalMonetaryTotal><LineExtensionAmount>100.00</LineExtensionAmount>
  <TaxExclusiveAmount>100.00</TaxExclusiveAmount><TaxInclusiveAmount>119.00</TaxInclusiveAmount>
  <PayableAmount>119.00</PayableAmount></LegalMonetaryTotal>
<InvoiceLine><ID>1</ID><InvoicedQuantity unitCode="C62">1</InvoicedQuantity>
  <LineExtensionAmount>100.00</LineExtensionAmount>
  <Item><Name>Widget</Name><ClassifiedTaxCategory><ID>S</ID><Percent>19</Percent></ClassifiedTaxCategory></Item>
  <Price><PriceAmount>100.00</PriceAmount></Price></InvoiceLine>
</Invoice>`

func main() {
	// An invoice whose seller has no registered name breaks BR-06.
	broken := strings.Replace(exampleUBL, "<RegistrationName>Seller Ltd</RegistrationName>", "", 1)

	report, err := formalis.Validate(context.Background(), []byte(broken), formalis.ProfileEN16931)
	if err != nil {
		fmt.Println("could not read it:", err)
		return
	}
	for _, v := range report.Violations {
		fmt.Printf("%s %s (%s)\n", v.Source, v.Rule, v.Severity)
	}

}
Output:
EN 16931 BR-06 (fatal)

func ValidateCIUS

func ValidateCIUS(ctx context.Context, xmlData []byte) (Report, error)

ValidateCIUS validates an invoice against whichever rule set the document itself declares, falling back to the EN 16931 core when it declares none. It routes both syntaxes (CII and UBL).

It applies the same arbitration Detect does — the one function, route, reached with the facts each caller already has — so the rule set this runs and the one Detect names for the same bytes cannot differ. Before that was so, they did: every PINT invoice was routed here to the Peppol BIS Billing 3.0 validator while Detect reported PINT (C24), and 154 documents in the conformance corpus were validated against a rule set Detect disagreed with.

What that means in practice is that this is no longer a CIUS-only dispatcher, though the name is kept: a document whose Specification identifier names PINT, OIOUBL or UBL-TR is validated against that rule set, and a UBL document that declares no identifier but carries a format's distinguishing mark — a ZATCA profile identifier, an ebInterface Biller, a Svefaktura SellerParty — is validated against that one. Every one of those was previously checked against the EN 16931 core, which is a rule set they do not claim to follow and which therefore reported findings that were not defects.

ctx bounds how long the call may take; the work itself is bounded by this package's own limits. A cancelled run reports a RuleLimit violation rather than an empty Violations slice, so a run that stopped early cannot be read as a clean invoice or credit note.

The error is for input that could not be read at all — XML that is not well-formed, or a character encoding this package does not implement. It is a statement about the file rather than about the document, and the Report returned with it is the zero Report, so a caller who ignores the error cannot read the value as clean. See ErrMalformedXML.

The Report's coverage follows the document, not this entry point: it names the gaps of the rule set the dispatch actually ran, so an XRechnung invoice comes back with the XRechnung gaps and a Portuguese one with the CIUS-PT gaps. A document that declares nothing recognisable is validated against the EN 16931 core and reports the core's gaps alone.

func ValidateCIUSPT added in v0.2.0

func ValidateCIUSPT(ctx context.Context, xmlData []byte) (Report, error)

ValidateCIUSPT validates an invoice XML against the Portuguese CIUS-PT: the EN 16931 core plus every rule AT/eSPap publishes — the 65 BR-CIUS-PT-* business rules, AT's own eight BR-AA-*, and the 290 DT-CIUS-PT-* datatype and arithmetic rules.

The EN 16931 core accepts either syntax. The CIUS-PT rules are evaluated for a UBL document only, because that is the only binding AT/eSPap publishes: a CII invoice is validated against the core and reported as carrying no CIUS-PT finding, which is what a reference CIUS-PT validator says about it too.

For a UBL document the core is evaluated with AT/eSPap's own condition wherever AT/eSPap wrote one — nine CEN identifiers, listed in ciusCENCopyVerdicts. Those findings keep SourceEN16931 and CEN's identifier, and carry Reading == SourceCIUSPT. No other entry point substitutes them.

ctx bounds how long the call may take; the work itself is bounded by this package's own limits. A cancelled run reports a RuleLimit violation rather than an empty Violations slice, so a run that stopped early cannot be read as a clean invoice or credit note.

The error is for input that could not be read at all — XML that is not well-formed, or a character encoding this package does not implement. It is a statement about the file rather than about the document, and the Report returned with it is the zero Report, so a caller who ignores the error cannot read the value as clean. See ErrMalformedXML.

The Report names the rule families neither rule set evaluates — the union of Coverage(SourceEN16931) and Coverage(SourceCIUSPT). Coverage(SourceCIUSPT) is empty, so a clean Portuguese invoice reports Conformant() == true and Complete() == true; what remains in the union is the seven rules CEN publishes that no validator can evaluate, which carry Unevaluable and do not hold the verdict down.

"Clean" there means clean against the union, which is stricter than AT/eSPap's own validator: AT vendored CEN's rules in 2018 and left 114 of them out, and this package evaluates CEN's current set regardless. A caller who needs "what a reference CIUS-PT validator would say" rather than "is this both EN 16931- and CIUS-PT-conformant" should read Violation.Rule against ciusCENCopyOmissions. See the file comment above.

func ValidateCIUSRO added in v0.2.0

func ValidateCIUSRO(ctx context.Context, xmlData []byte) (Report, error)

ValidateCIUSRO validates an invoice XML against the Romanian CIUS-RO: the EN 16931 core plus the CIUS-RO mandatory-term rules.

The EN 16931 core accepts either syntax. The CIUS-RO rules are evaluated for a UBL document only, because that is the only binding ANAF publishes: a CII invoice is validated against the core and reported as carrying no CIUS-RO finding, which is what a reference CIUS-RO validator says about it too.

ctx bounds how long the call may take; the work itself is bounded by this package's own limits. A cancelled run reports a RuleLimit violation rather than an empty Violations slice, so a run that stopped early cannot be read as a clean invoice or credit note.

The error is for input that could not be read at all — XML that is not well-formed, or a character encoding this package does not implement. It is a statement about the file rather than about the document, and the Report returned with it is the zero Report, so a caller who ignores the error cannot read the value as clean. See ErrMalformedXML.

The Report names the rule families neither rule set evaluates — the union of Coverage(SourceEN16931) and Coverage(SourceCIUSRO).

func ValidateEN16931 added in v0.3.0

func ValidateEN16931(ctx context.Context, xmlData []byte) (Report, error)

ValidateEN16931 validates an invoice XML against CEN's EN 16931 and nothing else: the semantic model's business rules and CEN's own syntax binding for whichever of the two syntaxes the document is in.

It is Validate without the Factur-X reading, and it takes no Profile because CEN publishes none — the five data-richness tiers are FNFE-MPE's, and naming one is what asks for Factur-X's rule set instead of CEN's. A caller with a Factur-X document and its profile wants Validate; a caller asking "is this a conforming EN 16931 CII or UBL invoice", which is what a CEN reference validator answers, wants this.

It is exactly what Validate with ProfileEN16931 did before Profile began selecting a binding, and the EN 16931 tier is where the difference is smallest: the two rule sets agree on the whole BR-* core there and differ only in the binding. They differ most at EXTENDED, whose whole purpose is to carry terms the EN 16931 core subset of CII does not have, and which CEN's binding therefore reports.

Everything else — the error contract, the cancellation contract, the coverage the Report names — is Validate's, which documents it.

func ValidateEbInterface added in v0.2.0

func ValidateEbInterface(ctx context.Context, xmlData []byte) (Report, error)

ValidateEbInterface validates an Austrian ebInterface document against its mandatory structure.

ctx bounds how long the call may take; the work itself is bounded by this package's own limits. A cancelled run reports a RuleLimit violation rather than an empty Violations slice, so a run that stopped early cannot be read as a clean invoice.

The error is for input that could not be read at all — XML that is not well-formed, or a character encoding this package does not implement. It is a statement about the file rather than about the document, and the Report returned with it is the zero Report, so a caller who ignores the error cannot read the value as clean. See ErrMalformedXML.

This validator checks the mandatory structure and code lists rather than the whole schema its authority publishes, so the Report is never Conformant even for a document with no findings: Report.NotEvaluated, from Coverage(SourceEbInterface), says what was not checked.

func ValidateFacturae added in v0.2.0

func ValidateFacturae(ctx context.Context, xmlData []byte) (Report, error)

ValidateFacturae validates a Spanish Facturae document against its mandatory structure and Spanish code lists.

ctx bounds how long the call may take; the work itself is bounded by this package's own limits. A cancelled run reports a RuleLimit violation rather than an empty Violations slice, so a run that stopped early cannot be read as a clean invoice.

The error is for input that could not be read at all — XML that is not well-formed, or a character encoding this package does not implement. It is a statement about the file rather than about the document, and the Report returned with it is the zero Report, so a caller who ignores the error cannot read the value as clean. See ErrMalformedXML.

This validator checks the mandatory structure and code lists rather than the whole schema its authority publishes, so the Report is never Conformant even for a document with no findings: Report.NotEvaluated, from Coverage(SourceFacturae), says what was not checked.

func ValidateFatturaPA added in v0.2.0

func ValidateFatturaPA(ctx context.Context, xmlData []byte) (Report, error)

ValidateFatturaPA validates an Italian FatturaPA / FatturaElettronica document against its mandatory structure and Italian code lists.

ctx bounds how long the call may take; the work itself is bounded by this package's own limits. A cancelled run reports a RuleLimit violation rather than an empty Violations slice, so a run that stopped early cannot be read as a clean invoice.

The error is for input that could not be read at all — XML that is not well-formed, or a character encoding this package does not implement. It is a statement about the file rather than about the document, and the Report returned with it is the zero Report, so a caller who ignores the error cannot read the value as clean. See ErrMalformedXML.

This validator checks the mandatory structure and code lists rather than the whole schema its authority publishes, so the Report is never Conformant even for a document with no findings: Report.NotEvaluated, from Coverage(SourceFatturaPA), says what was not checked.

func ValidateFinvoice added in v0.2.0

func ValidateFinvoice(ctx context.Context, xmlData []byte) (Report, error)

ValidateFinvoice validates a Finnish Finvoice document against its mandatory structure.

ctx bounds how long the call may take; the work itself is bounded by this package's own limits. A cancelled run reports a RuleLimit violation rather than an empty Violations slice, so a run that stopped early cannot be read as a clean invoice.

The error is for input that could not be read at all — XML that is not well-formed, or a character encoding this package does not implement. It is a statement about the file rather than about the document, and the Report returned with it is the zero Report, so a caller who ignores the error cannot read the value as clean. See ErrMalformedXML.

This validator checks the mandatory structure and code lists rather than the whole schema its authority publishes, so the Report is never Conformant even for a document with no findings: Report.NotEvaluated, from Coverage(SourceFinvoice), says what was not checked.

func ValidateKSeF added in v0.2.0

func ValidateKSeF(ctx context.Context, xmlData []byte) (Report, error)

ValidateKSeF validates a Polish KSeF FA document against its mandatory structure and Polish code lists.

ctx bounds how long the call may take; the work itself is bounded by this package's own limits. A cancelled run reports a RuleLimit violation rather than an empty Violations slice, so a run that stopped early cannot be read as a clean invoice.

The error is for input that could not be read at all — XML that is not well-formed, or a character encoding this package does not implement. It is a statement about the file rather than about the document, and the Report returned with it is the zero Report, so a caller who ignores the error cannot read the value as clean. See ErrMalformedXML.

This validator checks the mandatory structure and code lists rather than the whole schema its authority publishes, so the Report is never Conformant even for a document with no findings: Report.NotEvaluated, from Coverage(SourceKSeF), says what was not checked.

func ValidateNLCIUS

func ValidateNLCIUS(ctx context.Context, xmlData []byte) (Report, error)

ValidateNLCIUS validates an invoice XML against the Dutch NLCIUS (SimplerInvoicing) CIUS: the EN 16931 core plus the NLCIUS-specific rules. It accepts either syntax, and it is the one CIUS in this package of which that is true because its authority publishes both bindings rather than because this package assumed so.

The BR-NL rules apply to a document that declares the NLCIUS customization identifier, and all but BR-NL-13 additionally require the supplier to be in the Netherlands. Those are the artefact's own two conditions, $si and $s.

ctx bounds how long the call may take; the work itself is bounded by this package's own limits. A cancelled run reports a RuleLimit violation rather than an empty Violations slice, so a run that stopped early cannot be read as a clean invoice or credit note.

The error is for input that could not be read at all — XML that is not well-formed, or a character encoding this package does not implement. It is a statement about the file rather than about the document, and the Report returned with it is the zero Report, so a caller who ignores the error cannot read the value as clean. See ErrMalformedXML.

The Report names the rule families neither rule set evaluates. NLCIUS is the one CIUS here whose fatal rules are implemented in full; its entry in the coverage table is the advisory half alone. The EN 16931 core it runs on is another matter — see Coverage(SourceEN16931).

func ValidateOIOUBL added in v0.2.0

func ValidateOIOUBL(ctx context.Context, xmlData []byte) (Report, error)

ValidateOIOUBL validates a Danish OIOUBL Invoice against its mandatory structure.

ctx bounds how long the call may take; the work itself is bounded by this package's own limits. A cancelled run reports a RuleLimit violation rather than an empty Violations slice, so a run that stopped early cannot be read as a clean invoice.

The error is for input that could not be read at all — XML that is not well-formed, or a character encoding this package does not implement. It is a statement about the file rather than about the document, and the Report returned with it is the zero Report, so a caller who ignores the error cannot read the value as clean. See ErrMalformedXML.

This validator checks the mandatory structure and code lists rather than the whole schema its authority publishes, so the Report is never Conformant even for a document with no findings: Report.NotEvaluated, from Coverage(SourceOIOUBL), says what was not checked.

func ValidateOSA added in v0.2.0

func ValidateOSA(ctx context.Context, xmlData []byte) (Report, error)

ValidateOSA validates a Hungarian OSA invoice-data document against its mandatory structure.

ctx bounds how long the call may take; the work itself is bounded by this package's own limits. A cancelled run reports a RuleLimit violation rather than an empty Violations slice, so a run that stopped early cannot be read as a clean invoice-data report.

This validator checks the mandatory structure and code lists rather than the whole schema its authority publishes, so the Report is never Conformant even for a document with no findings: Report.NotEvaluated, from Coverage(SourceOSA), says what was not checked.

func ValidateOrderXML

func ValidateOrderXML(ctx context.Context, xmlData []byte) (Report, error)

ValidateOrderXML checks an embedded Cross Industry Order's structure and mandatory head business terms. An Order-X is an order, not an invoice: none of the EN 16931 invoice rules apply to it, and the five ORDER-* checks here are this package's own.

ctx bounds how long the call may take; the work itself is bounded by this package's own limits. A cancelled run reports a RuleLimit violation rather than an empty Violations slice, so a run that stopped early cannot be read as a clean order.

The error is for input that could not be read at all — XML that is not well-formed, or a character encoding this package does not implement. It is a statement about the file rather than about the document, and the Report returned with it is the zero Report, so a caller who ignores the error cannot read the value as clean. See ErrMalformedXML.

This validator checks the mandatory structure and code lists rather than the whole schema its authority publishes, so the Report is never Conformant even for a document with no findings: Report.NotEvaluated, from Coverage(SourceOrderX), says what was not checked.

func ValidatePINT added in v0.2.0

func ValidatePINT(ctx context.Context, xmlData []byte) (Report, error)

ValidatePINT validates a Peppol PINT document against the mandatory structure shared by every jurisdiction.

ctx bounds how long the call may take; the work itself is bounded by this package's own limits. A cancelled run reports a RuleLimit violation rather than an empty Violations slice, so a run that stopped early cannot be read as a clean invoice or credit note.

The error is for input that could not be read at all — XML that is not well-formed, or a character encoding this package does not implement. It is a statement about the file rather than about the document, and the Report returned with it is the zero Report, so a caller who ignores the error cannot read the value as clean. See ErrMalformedXML.

This validator checks the mandatory structure and code lists rather than the whole schema its authority publishes, so the Report is never Conformant even for a document with no findings: Report.NotEvaluated, from Coverage(SourcePINT), says what was not checked.

func ValidatePeppol

func ValidatePeppol(ctx context.Context, xmlData []byte) (Report, error)

ValidatePeppol validates an invoice XML against the OpenPEPPOL BIS Billing 3.0 CIUS: the EN 16931 core plus the Peppol-specific rules. It accepts either syntax.

ctx bounds how long the call may take; the work itself is bounded by this package's own limits. A cancelled run reports a RuleLimit violation rather than an empty Violations slice, so a run that stopped early cannot be read as a clean invoice or credit note.

The error is for input that could not be read at all — XML that is not well-formed, or a character encoding this package does not implement. It is a statement about the file rather than about the document, and the Report returned with it is the zero Report, so a caller who ignores the error cannot read the value as clean. See ErrMalformedXML.

The Report names the rule families neither rule set evaluates, which is now Coverage(SourceEN16931) alone: Coverage(SourcePeppol) is empty. Both rule sets in the two vendored OpenPEPPOL binding files are evaluated, each identifier in the binding that publishes it —

  • the 59 PEPPOL-COMMON-* and PEPPOL-EN16931-* rules, and
  • the 101 country-specific rules the same files publish under a comment reading "National rules": DE-R-*, DK-R-*, GR-R-*/GR-S-*, IS-R-*, IT-R-*, NL-R-*, NO-R-* and SE-R-*.

244 (identifier, binding) pairs in total. So a clean Peppol invoice reports Conformant() == true.

The country rules are not an opt-in national profile: neither binding file declares a Schematron <phase>, buildconfig.xml's base configuration is the whole file, and each rule is gated inside itself on the supplier's country — and, for the domestic ones, the customer's. A French invoice matches none of their contexts. peppol_country_rules.go sets out the five different spellings of that gate, which are not interchangeable.

The severity of each finding is OpenPEPPOL's published flag, so a document can fail an advisory rule and still be Conformant: the identifier-format warnings of PEPPOL-COMMON-R044..R047 and R052/R053, and eighteen of the country rules, including every SE-R-007..012 and the Greek GR-S-008-1 and GR-S-011.

func ValidateSRBDT added in v0.2.0

func ValidateSRBDT(ctx context.Context, xmlData []byte) (Report, error)

ValidateSRBDT validates an invoice XML against the Serbian CIUS (SRBDT): the EN 16931 core plus the SRBDT mandatory-term, format and VAT-category rules.

The EN 16931 core accepts either syntax. The SRBDT rules are evaluated for a UBL document only, because that is the only binding the Ministry publishes: a CII invoice is validated against the core and reported as carrying no SRBDT finding, which is what a reference SRBDT validator says about it too.

ctx bounds how long the call may take; the work itself is bounded by this package's own limits. A cancelled run reports a RuleLimit violation rather than an empty Violations slice, so a run that stopped early cannot be read as a clean invoice or credit note.

The error is for input that could not be read at all — XML that is not well-formed, or a character encoding this package does not implement. It is a statement about the file rather than about the document, and the Report returned with it is the zero Report, so a caller who ignores the error cannot read the value as clean. See ErrMalformedXML.

The Report names the rule families neither rule set evaluates — the union of Coverage(SourceEN16931) and Coverage(SourceSRBDT). Every entry in the second is unevaluable, so a clean Serbian invoice reports Conformant.

func ValidateSvefaktura added in v0.2.0

func ValidateSvefaktura(ctx context.Context, xmlData []byte) (Report, error)

ValidateSvefaktura validates a Swedish Svefaktura document against its mandatory structure.

ctx bounds how long the call may take; the work itself is bounded by this package's own limits. A cancelled run reports a RuleLimit violation rather than an empty Violations slice, so a run that stopped early cannot be read as a clean invoice.

The error is for input that could not be read at all — XML that is not well-formed, or a character encoding this package does not implement. It is a statement about the file rather than about the document, and the Report returned with it is the zero Report, so a caller who ignores the error cannot read the value as clean. See ErrMalformedXML.

This validator checks the mandatory structure and code lists rather than the whole schema its authority publishes, so the Report is never Conformant even for a document with no findings: Report.NotEvaluated, from Coverage(SourceSvefaktura), says what was not checked.

func ValidateTEAPPS added in v0.2.0

func ValidateTEAPPS(ctx context.Context, xmlData []byte) (Report, error)

ValidateTEAPPS validates a Finnish TEAPPS batch against its mandatory structure.

ctx bounds how long the call may take; the work itself is bounded by this package's own limits. A cancelled run reports a RuleLimit violation rather than an empty Violations slice, so a run that stopped early cannot be read as a clean batch.

This validator checks the mandatory structure and code lists rather than the whole schema its authority publishes, so the Report is never Conformant even for a document with no findings: Report.NotEvaluated, from Coverage(SourceTEAPPS), says what was not checked.

func ValidateTurkishInvoice added in v0.2.0

func ValidateTurkishInvoice(ctx context.Context, xmlData []byte) (Report, error)

ValidateTurkishInvoice validates a Turkish UBL-TR Invoice against its mandatory structure.

ctx bounds how long the call may take; the work itself is bounded by this package's own limits. A cancelled run reports a RuleLimit violation rather than an empty Violations slice, so a run that stopped early cannot be read as a clean invoice.

The error is for input that could not be read at all — XML that is not well-formed, or a character encoding this package does not implement. It is a statement about the file rather than about the document, and the Report returned with it is the zero Report, so a caller who ignores the error cannot read the value as clean. See ErrMalformedXML.

This validator checks the mandatory structure and code lists rather than the whole schema its authority publishes, so the Report is never Conformant even for a document with no findings: Report.NotEvaluated, from Coverage(SourceUBLTR), says what was not checked.

func ValidateUBLBE added in v0.2.0

func ValidateUBLBE(ctx context.Context, xmlData []byte) (Report, error)

ValidateUBLBE validates an invoice XML against the Belgian UBL.BE CIUS: the EN 16931 core plus the UBL.BE-specific rules.

The EN 16931 core accepts either syntax. The ubl-BE rules are evaluated for a UBL document only, because UBL.BE is a UBL profile and every rule in it is bound to a UBL path: a CII invoice is validated against the core and reported as carrying no UBL.BE finding, which is what a reference UBL.BE validator says about it too.

ctx bounds how long the call may take; the work itself is bounded by this package's own limits. A cancelled run reports a RuleLimit violation rather than an empty Violations slice, so a run that stopped early cannot be read as a clean invoice or credit note.

The error is for input that could not be read at all — XML that is not well-formed, or a character encoding this package does not implement. It is a statement about the file rather than about the document, and the Report returned with it is the zero Report, so a caller who ignores the error cannot read the value as clean. See ErrMalformedXML.

The Report names the rule families neither rule set evaluates — the union of Coverage(SourceEN16931) and Coverage(SourceUBLBE).

func ValidateXRechnung

func ValidateXRechnung(ctx context.Context, xmlData []byte) (Report, error)

ValidateXRechnung validates an invoice XML against the XRechnung CIUS: the EN 16931 core (with the XRechnung sub-profile overrides applied) plus the BR-DE-* rules. It accepts either syntax.

ctx bounds how long the call may take; the work itself is bounded by this package's own limits. A cancelled run reports a RuleLimit violation rather than an empty Violations slice, so a run that stopped early cannot be read as a clean invoice or credit note.

The error is for input that could not be read at all — XML that is not well-formed, or a character encoding this package does not implement. It is a statement about the file rather than about the document, and the Report returned with it is the zero Report, so a caller who ignores the error cannot read the value as clean. See ErrMalformedXML.

The Report names the rule families neither rule set evaluates — the union of Coverage(SourceEN16931) and Coverage(SourceXRechnung). The XRechnung rule set is evaluated in full: KoSIT's own 57 identifiers, both sub-profiles included, and the 21 Peppol BIS Billing rules its released Schematron merges in. The severity of each finding is the flag the artefact publishes — eleven of KoSIT's fifty-seven are warning or information rather than fatal, and so is PEPPOL-EN16931-R120 as KoSIT re-flags it — so a document can fail one of them and still be Conformant.

The imported findings carry SourcePeppol, because OpenPEPPOL is the authority that wrote those rules and Source names the author. They are still part of the XRechnung rule set, so they are covered by Coverage(SourceXRechnung) rather than by Coverage(SourcePeppol): a caller reading this Report's NotEvaluated is told what the *XRechnung* Schematron leaves unchecked, and the rules Peppol publishes that XRechnung does not import are not that. See the Coverage table.

func ValidateZATCA added in v0.2.0

func ValidateZATCA(ctx context.Context, xmlData []byte) (Report, error)

ValidateZATCA validates a Saudi ZATCA UBL invoice against its KSA-mandatory structure.

ctx bounds how long the call may take; the work itself is bounded by this package's own limits. A cancelled run reports a RuleLimit violation rather than an empty Violations slice, so a run that stopped early cannot be read as a clean invoice or credit note.

The error is for input that could not be read at all — XML that is not well-formed, or a character encoding this package does not implement. It is a statement about the file rather than about the document, and the Report returned with it is the zero Report, so a caller who ignores the error cannot read the value as clean. See ErrMalformedXML.

This validator checks the mandatory structure and code lists rather than the whole schema its authority publishes, so the Report is never Conformant even for a document with no findings: Report.NotEvaluated, from Coverage(SourceZATCA), says what was not checked.

func (Report) Complete added in v0.2.0

func (r Report) Complete() bool

Complete reports whether every rule that *can* be evaluated was evaluated, advisory rules included. It is the stricter of the two questions, and it is false when either kind of gap is present:

  • the rule set that ran does not implement some family its authority publishes and a validator could check, which is a static fact about this package and is named in NotEvaluated, whatever that family's severity; or
  • this run stopped before it had seen everything — a cancelled context or a tripped resource budget — or never chose a rule set at all, which Violations reports as a finding IsCheckerViolation recognises.

It was a field until severity arrived and is a method now, because with severity on the family the two questions genuinely differ: Conformant asks whether the verdict is trustworthy and passes over advisory gaps, Complete asks whether this package saw as much as a reference validator would. A single boolean could answer only one of them, and it answered the stricter one, which is why a residue of advisory families would have kept Conformant false forever.

A family its authority made unevaluable does not make a run incomplete, and that clause is what makes this predicate answerable at all. Without it the question is "did this package evaluate every rule its authorities published", and for CEN the answer is permanently no — not because of anything this package could fix, but because CEN publishes seven rules it cannot honour itself, four of them bound to the XPath expression true(). A predicate whose answer no work can change tells a caller nothing, which is the state Conformant was in before severity moved onto the family. With the clause the question becomes "did this package see everything a reference validator could see", which is answerable, and as of the advisory bindings the EN 16931 core answers yes. See RuleFamily.Unevaluable for why that clause cannot be stretched.

A document that is not an invoice at all (RuleRoot) does not make a run incomplete on its own account: that is a definite finding about the file, and Conformant is false because of the finding rather than because of any doubt.

The zero Report is not Complete, for the same reason it is not Conformant and through the same unexported field. That mattered less while no rule set could reach Complete at all; now that one can, it is the only thing standing between a Report nobody filled in and the strongest claim this package makes. TestZeroReportIsNotComplete pins it directly rather than leaving it to follow from Conformant.

func (Report) Conformant added in v0.2.0

func (r Report) Conformant() bool

Conformant reports whether this document may be treated as conforming to the rule set it was validated against.

It is the conjunction the caller almost always means: nothing found that an authority rejects a document for, and nothing left unexamined that could have been. len(Violations) == 0 alone is the claim this package exists to stop people making, because it is equally true of a run that checked everything, a run that was cancelled, and a run whose rule set has fatal holes.

Three things make it false:

  • a fatal finding — the document breaks a rule its authority rejects for;
  • a finding IsCheckerViolation recognises — the run was cut short, or never chose a rule set, so the answer is "unknown" rather than "conformant";
  • a fatal family in NotEvaluated that is evaluable — a rule that could have rejected this document, that a validator could have checked, and that this package did not check.

Warnings do not: an advisory finding, and an advisory gap in the rule set, are both things a reference validator would report and no authority would reject an invoice for. Read them with Warnings and Complete respectively.

Nor does a fatal family its authority made unevaluable, and that exception is narrower than it sounds. It is not "we decided this one does not count": it means the published artefact cannot fail — CEN binds four of these to the XPath expression true() — or cannot be reached, so no validator anywhere reports it and no gateway rejects a document over it. Not checking a rule nothing can check cannot put a verdict in doubt. RuleFamily.Unevaluable is where the boundary is documented and two tests hold the table to it.

The checker's own findings are tested for by IsCheckerViolation and not merely by their severity, deliberately. They are fatal — see Severity — but a stopped run must be non-conformant because the checker did not look, which is a different fact from the finding's weight, and this predicate should not quietly start depending on a severity someone could reclassify.

A consequence worth stating plainly: every rule set in this package has gaps today, and a rule set with a *fatal* gap makes Conformant false for every document it validates, however clean. One rule set no longer has one. The EN 16931 core's fatal half is complete — every fatal rule of the semantic model, of the UBL binding and of the CII binding is evaluated, bar the handful CEN's own reference implementation cannot report — so a clean invoice validated against the core alone, by ValidateEN16931 or by ValidateCIUS on a document declaring no CIUS, is conformant — and Complete as well, which no rule set in this package had ever been: everything left in Coverage(SourceEN16931) is a rule CEN itself cannot honour. Every other Source still names a fatal gap it could close and has not, so Conformant is still false for every document a CIUS or national validator is handed.

Validate is now among them, and the change is worth stating rather than leaving to be discovered. It used to return the sentence above because it applied CEN's binding to a Factur-X document; Factur-X publishes a binding of its own and does not adopt CEN's, so what it was reporting Conformant on was a verdict against the wrong authority's rules. That binding — the per-profile data model, 2,159 assertions across the five tiers — is evaluated now, and Validate stays here on the strength of one smaller entry: the 42 BR-FXEXT-* rules that restate a CEN identifier the EXTENDED profile drops, whose stricter CEN originals this package does evaluate. A caller who wants the old answer wants ValidateEN16931, which is what the old call actually was. Coverage says why for any Source, and Report.NotEvaluated says why for any particular run. A caller who wants the older, weaker claim writes len(r.Fatal()) == 0 and now has r.NotEvaluated sitting beside it, naming exactly what that claim omits.

func (Report) Fatal added in v0.2.0

func (r Report) Fatal() []Violation

Fatal returns the findings whose rules their authority rejects a document for, plus this checker's own findings, which are fatal for the reason Severity gives. It returns nil when there are none.

Fatal and Warnings partition Violations, so a caller that handles both handles everything. Neither is a substitute for Conformant: a document can have no fatal findings and still not be conformant, because a rule that would have rejected it was never evaluated.

func (Report) Warnings added in v0.2.0

func (r Report) Warnings() []Violation

Warnings returns the findings whose rules their authority reports without rejecting the document — CEN's flag="warning" and its equivalents. It returns nil when there are none.

These are information, not a verdict. A document whose only findings are warnings is conformant to the rule set that ran.

type RuleFamily added in v0.2.0

type RuleFamily struct {
	// Rules is the identifier or range the authority uses — "BR-DE-23-a/b,
	// BR-DE-24-a/b", "BR-CIUS-PT-24..63", "BR-NL-19..35". It is the machine-ish
	// half, written the way the authority writes it, ranges included, so that the
	// tests holding this table to the published Schematron can read identifiers
	// out of it and a caller can search for one.
	Rules string

	// Severity is the flag the authority put on these rules: fatal when it
	// rejects a document for breaking one, warning when it does not. It is a
	// quotation and never an estimate — not of how much the gap matters, and not
	// of what it costs a verdict. Where the authority publishes no flag because it
	// publishes no rule identifier (a format checked against its own schema), the
	// severity is this package's classification for the reason Severity documents.
	Severity Severity

	// Unevaluable reports that the authority published these rules and no
	// validator can evaluate them — the authority's own reference implementation
	// included. It is a fact about the published artefact, not about this package,
	// and it is what lets Report.Complete be reachable: a rule nobody can check is
	// not a rule this package skipped.
	//
	// It is deliberately narrow, and the narrowness is the point. It means the
	// artefact makes the rule unreachable or vacuous, demonstrably, from the
	// artefact itself: CEN binds BR-CO-05..08 to the XPath expression true(), so
	// the assertion cannot fail; CEN's ISO Schematron gives a node to the first
	// matching rule in a pattern and //ram:TypeCode precedes the rule
	// CII-DT-010/011/012 are bound to, so no processor reaches them; CEN's UBL
	// test for BR-51 is a string-length bound that a correctly masked card number
	// trips, so honouring it would mean accusing conforming invoices. Each of
	// those is checkable against a file in this repository by a reviewer who
	// disagrees, and Reason has to say which file and which construct.
	//
	// It does not mean hard, expensive, low-value, out of scope, or not yet. A
	// rule this package could implement and has not is a gap with Unevaluable
	// false, whatever the excuse — including the four CIUS whose authority
	// publishes no Schematron this repository vendors, which are unimplemented and
	// not unevaluable. TestUnevaluableFamiliesNameTheirEvidence and
	// TestOnlyEN16931HasUnevaluableFamilies keep that boundary from eroding as
	// later work adds entries, because a field that quietly widens to "we did not
	// do this one" would make Complete a lie in exactly the way the old Complete
	// field was.
	Unevaluable bool

	// Reason is why it is not evaluated, in prose. It is where a judgement gets
	// written down — that a rule is unenforceable by construction, that a
	// sub-profile is out of scope, that an authority publishes a schema rather
	// than a rule set — and for an Unevaluable family it is where the evidence
	// goes, specific enough to check against the vendored artefact without
	// re-deriving it.
	Reason string
}

RuleFamily names a group of rules an authority publishes and this package does not evaluate, in a form a caller can look up in that authority's own documentation.

Three facts, three fields

A gap has three independent properties, and the type carries one field for each because collapsing any two of them has already gone wrong here.

Severity is the authority's published flag, quoted, unconditionally. It is not this package's estimate of what the gap costs. Every entry in the table can be checked against the artefact its authority publishes wherever this repository vendors one, and TestCoverageSeveritiesMatchThePublishedFlag does exactly that with no exceptions at all.

Unevaluable is whether the authority published a rule *nobody* can evaluate — see the field's own comment, which is where the boundary is drawn, because it is the field most likely to be abused.

Reason is the prose, and for an unevaluable family it is where the evidence goes.

Why they cannot be collapsed

Severity and Unevaluable were one column until D10. To keep Report.Conformant from being false forever over rules CEN itself cannot honour, six entries CEN flags fatal were recorded at SeverityWarning, and the contradiction was kept legal by a hand-maintained list of those six identifiers inside the test that checks the column against the published flag. A column that needs an excuse list is answering two questions, and the excuse list is the load-bearing part: it had to be edited by hand every time such a rule was found, which is the same failure mode as a coverage claim in a file comment.

The cost of that collapse was not only tidiness. Report.Complete could never be true for any document, because CEN publishes seven rules nobody can evaluate and the table could not say so in a form a predicate could read — so the question "did this package see everything a reference validator would" had a permanent answer of no, for a reason a caller could not tell apart from "not implemented yet". That is the trap Conformant was in before severity arrived, one level down.

A third Severity value was rejected rather than overlooked. Unevaluability is orthogonal to severity, not a further point on the same scale, and Severity's own comment argues against a third value for a reason that applies here too: it would stop Report.Fatal and Report.Warnings being a partition of Violations.

func Coverage added in v0.2.0

func Coverage(src Source) []RuleFamily

Coverage returns the rule families that src publishes and this package does not evaluate, each with what its absence costs a conformance claim. It returns nil for a Source whose rule set is implemented in full, and for SourceChecker, which publishes no rules — its identifiers (RuleLimit, RuleProfile, RuleRoot) are this package's statements about its own run and about the file.

The result is a fresh slice: the table is package state read by every validator, and a caller that sorted or appended to it in place would change what every later Report says.

It parses nothing and takes no document, so it answers before a call is made — "is this validator good enough for what I am about to trust it with?" — as well as after, through Report.NotEvaluated, which is built from this same table.

Example

ExampleCoverage answers "what will that validator not look at?" before a call is made. It takes no document and cannot fail.

The severity on each family is what a caller acts on: a fatal gap means a rule that could have rejected this document was never evaluated, so Report.Conformant cannot be true. Coverage(SourceFatturaPA) is one such gap — the SdI publishes a whole XSD and this package checks the mandatory structure and Italian code lists — which is why a clean FatturaPA document is reported as not conformant.

Unevaluable is the other half of the answer, and it is the difference between a rule this package has not implemented and one nobody can implement. The two sources below show the two kinds side by side. The FatturaPA entry does not carry it, so it holds Conformant and Complete down: the SdI's own checks would reject documents this package passes. Every Coverage(SourceNLCIUS) entry does carry it — SimplerInvoicing publishes four assertions whose Schematron rule an earlier rule of the same pattern has already claimed, so no validator, its own included, ever reaches them — and those hold neither predicate down. So a Dutch invoice with no findings is reported Conformant and Complete, and an Italian one is reported neither.

package main

import (
	"fmt"

	"github.com/mgilbir/formalis"
)

func main() {
	for _, gap := range formalis.Coverage(formalis.SourceFatturaPA) {
		fmt.Printf("not evaluated: %s [%s]\n", gap.Rules, gap.Severity)
	}
	for _, gap := range formalis.Coverage(formalis.SourceNLCIUS) {
		fmt.Printf("not evaluated: %s [%s, unevaluable=%t]\n", gap.Rules, gap.Severity, gap.Unevaluable)
	}

}
Output:
not evaluated: the SdI FatturaPA XSD and the SdI's consistency checks [fatal]
not evaluated: BR-NL-9, in the CII binding only [fatal, unevaluable=true]
not evaluated: BR-NL-31, in the CII binding only [warning, unevaluable=true]
not evaluated: BR-NL-32-2, BR-NL-32-3, in the UBL binding only [warning, unevaluable=true]

type Severity added in v0.2.0

type Severity int

Severity is how much weight the authority that wrote a rule puts on breaking it: enough to reject the document, or not.

It is a property of the rule and not of the run or of the document, and it is not this package's opinion. CEN writes it into the Schematron as flag="fatal" or flag="warning" on every assertion, and the national authorities quoted here do the same (KoSIT adds flag="information", which is advisory under another name). This type carries that flag folded onto two values, because two is what a caller can act on: a fatal finding is a reason to refuse an invoice and a warning is not.

The same assertion can carry different flags in the two EN 16931 syntax bindings — BR-51 is fatal in the CII binding and a warning in the UBL one — so severity belongs on the finding, where the binding that produced it is known, rather than on a table keyed by rule identifier.

Why SeverityFatal is the zero value

An unstamped Severity reads as fatal, deliberately. The two orderings are not symmetric. With SeverityWarning at zero, an emission site that forgot the field would report a genuine non-conformance as advisory: Report.Conformant would pass over it, Report.Fatal would omit it, and an invoice would ship on the strength of a field nobody set. With SeverityFatal at zero the same omission over-reports — a caller sees a blocking finding for an advisory rule, notices, and the stamp gets fixed. This package exists to keep silence from being read as a clean invoice, so the default has to lean the other way.

The default is not a substitute for deciding, and it is not left unchecked: TestOnlyTheAdvisoryBindingsAreEmittedAsWarnings sweeps the whole corpus and fails on any finding whose severity does not match the half of the package it came from, and TestEveryEmittedEN16931RuleCarriesCENsFlag holds the one Source with vendored ground truth to the flag CEN publishes — in both directions, so an advisory rule stamped fatal by the zero value fails as loudly as a fatal one stamped advisory.

This package's own rule identifiers

Most national formats publish no rule identifier this package could quote, so the identifiers under those Sources — FPA-*, FE-*, EB-*, KS-*, FI-*, ZA-*, SV-*, TP-*, OIO-*, TR-*, HU-*, PINT-*, ORDER-* — were minted here (see Source). No authority has flagged them, so their severity is a decision rather than a quotation, and the decision is that all of them are fatal.

The reason is what those rules check rather than who wrote them down. Each is a mandatory element of the format's own schema or a value outside its own code list, so a document that breaks one is a document the authority's gateway rejects at the border: the SdI refuses a FatturaPA with no invoice number, and Fatoora refuses a ZATCA invoice with no UUID. That is precisely what fatal means here. If one of these formats is later found to publish a genuinely advisory expectation and this package implements it, it belongs at SeverityWarning and TestOnlyTheAdvisoryBindingsAreEmittedAsWarnings is where the change gets recorded.

SourceChecker findings

RuleLimit, RuleProfile and RuleRoot are this package's statements about its own run or about the file it was handed, so no authority has flagged them and severity is this package's classification rather than a quotation. They are fatal, for the reason above rather than by analogy: a caller who writes len(r.Fatal()) == 0 as their release gate must not have a cancelled run, a rejected Profile or a document that is not an invoice slip through it. A third value for "not a rule" was considered and rejected — it would make Fatal and Warnings no longer a partition of Violations, and every caller who switched on severity would have to invent a policy for the third case, most of them choosing the unsafe one. The distinction those callers actually need is already exported, precisely and tested: IsCheckerViolation.

const (
	// SeverityFatal is a rule its authority rejects a document for breaking.
	SeverityFatal Severity = iota
	// SeverityWarning is a rule its authority reports without rejecting: CEN's
	// flag="warning", KoSIT's flag="information", NLCIUS's "not recommended".
	// A document with warnings and no fatal finding is conformant.
	SeverityWarning
)

func (Severity) String added in v0.2.0

func (s Severity) String() string

type Source added in v0.2.0

type Source string

Source identifies the authority that defines a rule, so that a rule identifier is unique within it. Two authorities may mint the same string; (Source, Rule) is what identifies a rule, and Rule alone is not an identity.

The distinction is not decorative. This package reports identifiers minted by CEN, by seven national bodies, and by itself, in one flat string field, and they have already collided: the Order-X validator once emitted "BR-O-01" for "an Order shall have an order number" while the EN 16931 rule engine emitted "BR-O-01" for the "Not subject to VAT" category family. A caller aggregating findings across a mailbox keyed by Rule merged two unrelated defects, and a suppression list ("we accept BR-O-03 from this supplier") suppressed the wrong thing in the other document type. Scoping every finding by its author makes that class of mistake impossible to express.

Three judgement calls are recorded here because they are not obvious:

  • The UBL and CII syntax-binding rules (UBL-DT-*, UBL-SR-*, CII-SR-*, CII-DT-*) carry SourceEN16931, not a Source of their own. Source names the *authority*, and CEN publishes the bindings (EN 16931-3-2, EN 16931-3-3) as normative parts of the same standard as the semantic model (EN 16931-1); they arrive in the same conformance artefacts this package is tested against, and the FP=0 oracle counts them among the rules it must catch. Splitting them would also make one finding's Source depend on the invoice's syntax, since the same defect — two disagreeing payment means codes — is UBL-SR-47 on a UBL invoice and CII-SR-467 on a CII one, which is a distinction the caller did not ask for. A caller that does want "syntax binding" separately has the prefix, which is already disjoint from the core BR-* space.

  • RuleLimit, RuleProfile and RuleRoot carry SourceChecker. They are statements by this checker — "I stopped early", "you named a profile I do not implement", "this is not an invoice" — rather than by any rule authority, so attributing them to CEN would be a lie, and leaving them unattributed would make Source unusable as a filter. See IsCheckerViolation for why that predicate still tests Rule alone. A document this package could not read at all produces no finding under any Source: it is an error, because there is no document to attribute anything about.

  • Most national formats below (FatturaPA, Facturae, ebInterface, KSeF, Finvoice, TEAPPS, OIOUBL, Svefaktura, ZATCA, NAV OSA, UBL-TR, PINT, Order-X) do not publish a rule identifier this package could quote, so the identifiers under those Sources — "FPA-number", "ZA-uuid", "ORDER-01" — were invented here. The Source is still the format they judge the document against, which is what a caller routing or suppressing by format needs; it is not a claim that the format's own documentation uses these names. The Sources whose identifiers *are* quoted from a published rule set are EN 16931, Factur-X, XRechnung, Peppol, NLCIUS, CIUS-PT, CIUS-RO, UBL.BE and SRBDT.

const (
	// SourceNone is the absent authority — no rule set. It is the zero Source,
	// and it is what Detect reports for a document it read and recognised as no
	// format this package validates; Detection.Recognised tests for it. No
	// Violation ever carries it: a finding with no Source is an emission site
	// that did not decide whose rule it reports, which
	// TestNoRuleIdentifierIsClaimedByTwoSources fails on.
	SourceNone Source = ""
	// SourceEN16931 is CEN's EN 16931 — the semantic model's core business rules
	// (BR-*, BR-CO-*, BR-CL-*, BR-DEC-*, BR-IC-*, and the VAT category families)
	// together with the UBL and CII syntax bindings (UBL-DT-*, UBL-SR-*, CII-SR-*,
	// CII-DT-*).
	SourceEN16931 Source = "EN 16931"
	// SourceFacturX is FNFE-MPE and FeRD's Factur-X / ZUGFeRD, the Franco-German
	// CII profile family. Its own identifiers are BR-FXEXT-*, the rule set the
	// EXTENDED profile adds on top of EN 16931.
	//
	// It is a Source rather than only a Profile because Factur-X binds EN 16931
	// with a rule set of its own and does not adopt CEN's CII syntax binding. Of
	// the 583 CII-SR-* and CII-DT-* assertions CEN publishes, the five Factur-X
	// Schematrons carry four (CII-SR-463..466, and CII-DT-097 in EXTENDED only);
	// in their place they carry a profile data-model layer of their own — 51
	// assertions at MINIMUM rising to 1,241 at EXTENDED — deciding, per profile,
	// which element may appear where, how often, and with which attributes. Those
	// are the same questions CEN's binding decides, and they get different answers,
	// which is the whole point of a profile family whose richest tier is defined by
	// carrying more than the EN 16931 core.
	//
	// So the identifiers a Factur-X document is judged by are mostly CEN's — the
	// BR-* core, which Factur-X republishes verbatim — and those keep
	// SourceEN16931, as CIUS-RO's copy of BR-27 does. This Source carries the
	// identifiers FNFE minted. See Validate, which is the entry point that selects
	// this rule set, and Coverage(SourceFacturX), which says what of it is not
	// evaluated.
	SourceFacturX Source = "Factur-X"
	// SourceXRechnung is the German KoSIT XRechnung CIUS (BR-DE-*).
	SourceXRechnung Source = "XRechnung"
	// SourcePeppol is OpenPEPPOL BIS Billing 3.0: PEPPOL-EN16931-* and
	// PEPPOL-COMMON-*, and the country-specific rules the same two Schematron files
	// publish under a comment reading "National rules" — DE-R-*, DK-R-*,
	// GR-R-*/GR-S-*, IS-R-*, IT-R-*, NL-R-*, NO-R-* and SE-R-*.
	//
	// Those last are OpenPEPPOL's own national rule sets and not the CIUS of the same
	// countries: NL-R-* is distinct from the BR-NL-* of SourceNLCIUS, and DE-R-* from
	// the BR-DE-* of SourceXRechnung, which is why they carry this Source and not
	// those.
	//
	// It is the one Source a validator for another authority emits: the released
	// XRechnung Schematron merges twenty-one PEPPOL-EN16931-* rules in, so
	// ValidateXRechnung reports them — under OpenPEPPOL's Source, because Source
	// names the authority that wrote the rule. It imports none of the country rules;
	// see the comment on the coverage table in report.go.
	SourcePeppol Source = "Peppol"
	// SourceNLCIUS is the Dutch SimplerInvoicing NLCIUS (BR-NL-*).
	//
	// SI-UBL includes CEN's Schematron from a copy of its own, in both bindings and
	// in the G-account extension. Every CEN condition in all three copies is one CEN
	// published at some release — none is Dutch — so nothing here is overridden, and
	// that is derived rather than assumed: see ciusCENCopyVerdicts.
	SourceNLCIUS Source = "NLCIUS"
	// SourceCIUSPT is the Portuguese AT/eSPap CIUS-PT: BR-CIUS-PT-*, and AT's own
	// BR-AA-* — eight rules for the "Lower rate" (AA) VAT category, written by
	// cloning CEN's BR-S-* template for a category code EN 16931 leaves out of
	// BT-118's restricted list. CEN publishes no BR-AA-* family, so the identifier
	// looks like CEN's and is not, and this Source is where it belongs.
	//
	// It is also the one authority whose *conditions* this package substitutes for
	// CEN's. AT/eSPap ships a copy of CEN's UBL binding in which nine CEN
	// identifiers carry a condition CEN never published, and under ValidateCIUSPT
	// those nine are evaluated as AT/eSPap wrote them. Such a finding keeps
	// SourceEN16931 and CEN's identifier and carries Reading == SourceCIUSPT; see
	// Violation.Reading and cius_overrides.go.
	//
	// The same copy is CEN's validation-1.1.0 of June 2018 and omits 192 CEN
	// identifiers this package evaluates — 114 CEN had already published and AT left
	// out, 78 CEN has added since. None is suppressed, so ValidateCIUSPT reports
	// fatal EN 16931 findings on documents a reference CIUS-PT validator accepts,
	// including all 20 instances AT publishes as conformant. The split, the release
	// pin and the reasoning are in ciusCENCopyOmissions.
	SourceCIUSPT Source = "CIUS-PT"
	// SourceCIUSRO is the Romanian ANAF RO e-Factura CIUS. Its identifiers are
	// BR-RO-* — the business rules, the BR-RO-L* length limits, the BR-RO-DT* date
	// formats and the BR-RO-A* occurrence limits — and BR-DEC-RO-*, ANAF's
	// decimal-place limits, which are the one family here whose prefix is not
	// BR-RO. A reader scoping on "BR-RO-" alone misses a fifth of the rule set.
	//
	// BR-27 is not among them although ANAF re-publishes it inside its own national
	// file: it is a CEN identifier and this package reports CEN's under
	// SourceEN16931, with CEN's condition.
	//
	// ANAF also ships a whole copy of CEN's UBL binding beside its own file, and
	// every one of the 930 CEN identifiers in it carries a condition CEN published
	// at some release — none is Romanian. So there is nothing to override here, and
	// that is a derived fact rather than a decision: see ciusCENCopyVerdicts.
	SourceCIUSRO Source = "CIUS-RO"
	// SourceUBLBE is the Belgian UBL.BE CIUS (ubl-BE-*).
	SourceUBLBE Source = "UBL.BE"
	// SourceSRBDT is the Serbian SRBDT CIUS (RSR-*).
	SourceSRBDT Source = "SRBDT"
	// SourceFatturaPA is the Italian SdI FatturaPA format (FPA-*).
	SourceFatturaPA Source = "FatturaPA"
	// SourceFacturae is the Spanish Facturae format (FE-*).
	SourceFacturae Source = "Facturae"
	// SourceEbInterface is the Austrian ebInterface format (EB-*).
	SourceEbInterface Source = "ebInterface"
	// SourceKSeF is the Polish KSeF FA(2) format (KS-*).
	SourceKSeF Source = "KSeF"
	// SourceFinvoice is the Finnish Finvoice format (FI-*).
	SourceFinvoice Source = "Finvoice"
	// SourceTEAPPS is the Finnish Tieto TEAPPSXML format (TP-*).
	SourceTEAPPS Source = "TEAPPS"
	// SourceOIOUBL is the Danish OIOUBL profile (OIO-*).
	SourceOIOUBL Source = "OIOUBL"
	// SourceSvefaktura is the Swedish Svefaktura format (SV-*).
	SourceSvefaktura Source = "Svefaktura"
	// SourceZATCA is the Saudi ZATCA e-invoicing profile (ZA-*).
	SourceZATCA Source = "ZATCA"
	// SourceOSA is the Hungarian NAV Online Számla format (HU-*).
	SourceOSA Source = "NAV OSA"
	// SourceUBLTR is the Turkish UBL-TR e-Fatura profile (TR-*).
	SourceUBLTR Source = "UBL-TR"
	// SourcePINT is the Peppol International (PINT) billing model (PINT-*).
	SourcePINT Source = "PINT"
	// SourceOrderX is the Franco-German Order-X order document (ORDER-*).
	SourceOrderX Source = "Order-X"
	// SourceChecker is this package speaking about its own run, or about the file
	// it was handed, rather than about any rule: RuleLimit, RuleProfile and
	// RuleRoot.
	SourceChecker Source = "checker"
)

type Violation

type Violation struct {
	Source   Source
	Rule     string
	Severity Severity
	Message  string

	// Reading names the authority whose *condition* for Rule was evaluated, when
	// that is not Source's own. It is SourceNone — the zero value, and the case on
	// every finding this package reports outside a CIUS with condition overrides —
	// when the rule was judged by the condition its own Source publishes.
	//
	// It exists because a national CIUS may ship a modified copy of CEN's
	// Schematron rather than referencing it, so a document validated under that
	// CIUS is judged by its authority's reading of a CEN rule. The finding is still
	// CEN's BR-02 or BR-S-02: the identifier was minted by CEN, means what CEN says
	// it means, and re-stamping it with the CIUS's Source would make one identifier
	// name two rules — the collision TestNoRuleIdentifierIsClaimedByTwoSources
	// exists to prevent. What changes is *whose condition decided it*, and that is
	// what this field carries. See cius_overrides.go.
	//
	// A caller gating on "no fatal finding" is unaffected. A caller reconciling
	// this package's verdict against a reference validator needs it: a
	// SourceEN16931/BR-S-02 with Reading == SourceCIUSPT will not reproduce under a
	// plain EN 16931 validation, and that is correct rather than a defect.
	Reading Source
}

Violation reports one way in which a document departs from a rule set. Source names the authority that defines the rule and Rule is that authority's identifier for it (e.g. SourceEN16931/"BR-CO-15", SourceNLCIUS/"BR-NL-1"); neither is an identity on its own. Severity is whether that authority rejects a document for it.

Example

ExampleViolation shows why findings must be aggregated on the pair (Source, Rule). Two authorities may mint the same identifier, and Rule alone is not an identity.

package main

import (
	"context"
	"fmt"

	"github.com/mgilbir/formalis"
)

// exampleUBL is a minimal EN 16931 UBL invoice declaring no CIUS.
const exampleUBL = `<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<CustomizationID>urn:cen.eu:en16931:2017</CustomizationID>
<ID>INV-1</ID><IssueDate>2024-01-15</IssueDate>
<InvoiceTypeCode>380</InvoiceTypeCode><DocumentCurrencyCode>EUR</DocumentCurrencyCode>
<AccountingSupplierParty><Party>
  <PostalAddress><Country><IdentificationCode>DE</IdentificationCode></Country></PostalAddress>
  <PartyTaxScheme><CompanyID>DE123456789</CompanyID><TaxScheme><ID>VAT</ID></TaxScheme></PartyTaxScheme>
  <PartyLegalEntity><RegistrationName>Seller Ltd</RegistrationName></PartyLegalEntity>
</Party></AccountingSupplierParty>
<AccountingCustomerParty><Party>
  <PostalAddress><Country><IdentificationCode>DE</IdentificationCode></Country></PostalAddress>
  <PartyLegalEntity><RegistrationName>Buyer Ltd</RegistrationName></PartyLegalEntity>
</Party></AccountingCustomerParty>
<TaxTotal><TaxAmount>19.00</TaxAmount>
  <TaxSubtotal><TaxableAmount>100.00</TaxableAmount><TaxAmount>19.00</TaxAmount>
    <TaxCategory><ID>S</ID><Percent>19</Percent></TaxCategory></TaxSubtotal>
</TaxTotal>
<LegalMonetaryTotal><LineExtensionAmount>100.00</LineExtensionAmount>
  <TaxExclusiveAmount>100.00</TaxExclusiveAmount><TaxInclusiveAmount>119.00</TaxInclusiveAmount>
  <PayableAmount>119.00</PayableAmount></LegalMonetaryTotal>
<InvoiceLine><ID>1</ID><InvoicedQuantity unitCode="C62">1</InvoicedQuantity>
  <LineExtensionAmount>100.00</LineExtensionAmount>
  <Item><Name>Widget</Name><ClassifiedTaxCategory><ID>S</ID><Percent>19</Percent></ClassifiedTaxCategory></Item>
  <Price><PriceAmount>100.00</PriceAmount></Price></InvoiceLine>
</Invoice>`

func main() {
	type key struct {
		Source formalis.Source
		Rule   string
	}

	counts := map[key]int{}
	for _, doc := range [][]byte{[]byte(exampleUBL), []byte(`<Invoice/>`)} {
		report, err := formalis.ValidateCIUS(context.Background(), doc)
		if err != nil {
			fmt.Println("could not read it:", err)
			return
		}
		for _, v := range report.Violations {
			counts[key{v.Source, v.Rule}]++
		}
	}

	// "BR-06" means one thing under EN 16931 and could mean another under any
	// other authority; a suppression list keyed on the string alone would
	// suppress both.
	fmt.Println("EN 16931 BR-06:", counts[key{formalis.SourceEN16931, "BR-06"}])
	fmt.Println("Peppol BR-06:  ", counts[key{formalis.SourcePeppol, "BR-06"}])

}
Output:
EN 16931 BR-06: 1
Peppol BR-06:   0

func (Violation) Error

func (v Violation) Error() string

Error renders the finding, severity included. The severity is in the string rather than only in the field because this type satisfies error: a caller that logs a Violation would otherwise present an advisory finding in exactly the same words as a blocking one, which is the confusion Severity exists to end. The authority whose condition was evaluated is in the string for the same reason: a caller who logs the finding would otherwise be unable to tell a BR-S-02 CEN's own condition reported from one AT/eSPap's copy of it reported, and those are different claims about the document.

Directories

Path Synopsis
internal
gen/facturx command
Command facturx generates facturx_datamodel_table.go from the five Factur-X profile Schematrons and the code databases FNFE ships beside them.
Command facturx generates facturx_datamodel_table.go from the five Factur-X profile Schematrons and the code databases FNFE ships beside them.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL