Peppol invoice regression testing

Which of your invoices does the next rule set break?

Peppol publishes new validation rules several times a year. The release notes tell you what changed. They cannot tell you whether it affects you — that depends on what your invoices actually contain.

No password. Metadata-only retention available from the first upload.

The same invoices travel through two validation gates. One path passes; a changed rule on the other path highlights a problem.
Same invoices. New rules. Changes revealed.

What a release note looks like

“Changed rule PEPPOL-COMMON-R052 and PEPPOL-COMMON-R053 to be errors instead of warnings.”

Peppol BIS Billing 3.0.21 release notes

Those two rules are named “Danish chamber of commerce number” and “Danish ERSTORG number”. Read that note and you check your Danish invoices, if you send any.

Release comparisonUBL invoices

3.0.20 → 3.0.21

Published
Replayed

565

Newly failing

41

Rules responsible
PEPPOL-COMMON-R053

37

PEPPOL-EN16931-R007

3

PEPPOL-COMMON-R052

1

Corpus
ubl · 565 docs
Engine
Saxon-HE
Source
docs.peppol.eu

Same invoices. Two rule sets. Every change traced.

37

of the 41 broken invoices were German.

Documents carrying a nine-digit company identifier tagged with the Danish ERSTORG scheme, where the rule wants DK followed by eight digits. A warning under 3.0.20. Fatal under 3.0.21.

INVOICEUBL · Peppol BIS BillingCOMPANYID · SCHEME 0198123456789REJECTEDRULE EXPECTSDK + 8 digitsDanish ERSTORG scheme3.0.20 · warning3.0.21 · fatal

A rule set is not a document you read.

It is a program that runs against your data. The only honest way to know what a new release does to your invoices is to run it against your invoices — which is the entire product, and the reason there is no clever shortcut in it.

The case no diff can see

A rule broke invoices without changing at all.

PEPPOL-EN16931-R007

Same invoice, both releases. Nothing about R007 is different between them — but the variable it reads was rewritten, so the same document now resolves to a value the rule rejects.

3.0.20

accepted
  1. Invoice carries
    ProfileID = billing:02:1.0
  2. $profile is defined as
    matches(ProfileID, 'billing:([0-9]{2}):1.0')
    any two-digit profile
  3. so $profile resolves to
    '02'
  4. R007 asserts
    $profile != 'Unknown'

3.0.21

rejected
  1. Invoice carries
    ProfileID = billing:02:1.0
  2. $profile is defined as
    'billing:01:1.0' → '01'
    'billing_with_response' → '02'
    else → 'Unknown'
    exact matches only
  3. so $profile resolves to
    'Unknown'
  4. R007 asserts
    $profile != 'Unknown'

Only the marked rows differ. R007 itself is identical in both releases — same id, same flag="fatal", same test expression. Twelve <let> variables changed in 3.0.21, two of them regular expressions.

This is why the product replays documents instead of reading changelogs. A tool comparing rule identifiers and severities between releases reports no change here — not from carelessness, but because there genuinely is none to find in the rule. The full report carries every rule responsible and the invoices each one broke.

Three ways to answer the question

What each method can actually tell you.

Can it tell you…Release
notes
Rule-id
diff
Replaying
the corpus
A rule changedyesyesyes
A rule changed severitysometimesyesyes
A rule changed behaviour without changingnonoyes
Whether any of it touches your invoicesnonoyes
Which documents, by filenamenonoyes
Answered on the day of releasenosometimesyes

● yes · ◐ sometimes · ○ no — methods, not products. The third column is the only one that has seen your documents.

How it works

Four steps, no modelling.

No heuristic and no prediction anywhere in it. The rule set is compiled and executed exactly as an access point would execute it.

  1. 01

    Pull

    Schematron for each rule set, from docs.peppol.eu — the authority, not a mirror and not a git tag.

  2. 02

    Compile

    Three ISO skeleton stages turn the .sch into SVRL-producing XSLT, with Saxon-HE.

  3. 03

    Run twice

    Your corpus through each rule set. 565 invoices take about eight seconds per rule set.

  4. 04

    Diff

    Every invoice whose fatal outcome changed, grouped by the rule responsible.

Limits

What it does not do.

Stated here rather than discovered later.

Only fatal transitions count as breakage

An invoice that passed and now fails is a regression. A new warning is reported separately and is not counted, because an access point will still accept the document.

A removed rule is never a regression

When a rule disappears, invoices it used to reject start passing. That is reported as newly passing, never as something to act on.

It needs your invoices

There is no useful answer without a corpus. The whole claim is that the question cannot be answered from a changelog, and that applies to us too.

It tells you what broke, not how to fix it

You get the rule, the mechanism, and the exact documents. Correcting them is a change in the system that produced them.

Early access

Find out before your counterparty does.

Upload a corpus and run it against the last two releases — that alone usually answers whether this is a problem you have. Revalidate is in early access while the first customers shape it, so pricing is agreed directly rather than picked off a page.