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 ¶
- Constants
- Variables
- func DetectPINTJurisdiction(customizationID string) string
- func IsCheckerViolation(v Violation) bool
- func IsEbInterface(xmlData []byte) (bool, error)
- func IsFacturae(xmlData []byte) (bool, error)
- func IsFatturaPA(xmlData []byte) (bool, error)
- func IsFinvoice(xmlData []byte) (bool, error)
- func IsKSeF(xmlData []byte) (bool, error)
- func IsOIOUBL(xmlData []byte) (bool, error)
- func IsOSA(xmlData []byte) (bool, error)
- func IsPINT(xmlData []byte) (bool, error)
- func IsSvefaktura(xmlData []byte) (bool, error)
- func IsTEAPPS(xmlData []byte) (bool, error)
- func IsTurkishInvoice(xmlData []byte) (bool, error)
- func IsZATCA(xmlData []byte) (bool, error)
- type CIUS
- type Detection
- type Profile
- type Report
- func Validate(ctx context.Context, xmlData []byte, profile Profile) (Report, error)
- func ValidateCIUS(ctx context.Context, xmlData []byte) (Report, error)
- func ValidateCIUSPT(ctx context.Context, xmlData []byte) (Report, error)
- func ValidateCIUSRO(ctx context.Context, xmlData []byte) (Report, error)
- func ValidateEN16931(ctx context.Context, xmlData []byte) (Report, error)
- func ValidateEbInterface(ctx context.Context, xmlData []byte) (Report, error)
- func ValidateFacturae(ctx context.Context, xmlData []byte) (Report, error)
- func ValidateFatturaPA(ctx context.Context, xmlData []byte) (Report, error)
- func ValidateFinvoice(ctx context.Context, xmlData []byte) (Report, error)
- func ValidateKSeF(ctx context.Context, xmlData []byte) (Report, error)
- func ValidateNLCIUS(ctx context.Context, xmlData []byte) (Report, error)
- func ValidateOIOUBL(ctx context.Context, xmlData []byte) (Report, error)
- func ValidateOSA(ctx context.Context, xmlData []byte) (Report, error)
- func ValidateOrderXML(ctx context.Context, xmlData []byte) (Report, error)
- func ValidatePINT(ctx context.Context, xmlData []byte) (Report, error)
- func ValidatePeppol(ctx context.Context, xmlData []byte) (Report, error)
- func ValidateSRBDT(ctx context.Context, xmlData []byte) (Report, error)
- func ValidateSvefaktura(ctx context.Context, xmlData []byte) (Report, error)
- func ValidateTEAPPS(ctx context.Context, xmlData []byte) (Report, error)
- func ValidateTurkishInvoice(ctx context.Context, xmlData []byte) (Report, error)
- func ValidateUBLBE(ctx context.Context, xmlData []byte) (Report, error)
- func ValidateXRechnung(ctx context.Context, xmlData []byte) (Report, error)
- func ValidateZATCA(ctx context.Context, xmlData []byte) (Report, error)
- type RuleFamily
- type Severity
- type Source
- type Violation
Examples ¶
Constants ¶
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.
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".
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 ¶
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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 ¶
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
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.
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.
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.
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.
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.
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
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
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.
func ProfileFor ¶
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 ¶
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 ¶
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
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
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
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
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
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
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
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
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 ¶
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
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
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 ¶
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
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 ¶
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
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
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
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
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
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 ¶
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
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
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
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
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
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 )
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 ¶
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.
Source Files
¶
- cius.go
- cius_be.go
- cius_overrides.go
- cius_overrides_table.go
- cius_pt.go
- cius_pt_datatype.go
- cius_pt_datatype_eval.go
- cius_pt_datatype_table.go
- cius_pt_rules.go
- cius_ro.go
- cius_ro_rules_table.go
- cius_rs.go
- detect.go
- ebinterface.go
- en16931_cii_rules.go
- en16931_codelists.go
- en16931_model.go
- en16931_syntax_advisory.go
- en16931_syntax_advisory_eval.go
- en16931_syntax_advisory_table.go
- en16931_ubl_rules.go
- en16931_vat.go
- facturae.go
- facturx.go
- facturx_datamodel.go
- facturx_datamodel_table.go
- facturx_en16931.go
- facturx_restatements.go
- fatturapa.go
- finvoice.go
- ksef.go
- limits.go
- nlcius.go
- nlcius_gaccount.go
- oioubl.go
- orderx.go
- osa.go
- peppol.go
- peppol_codelists.go
- peppol_country_rules.go
- peppol_identifiers.go
- peppol_rules.go
- pint.go
- report.go
- svefaktura.go
- teapps.go
- treevalidator.go
- turkey.go
- types.go
- xrechnung.go
- xrechnung_rules.go
- zatca.go
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. |