<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Boban Mitevski</title>
    <description>The latest articles on DEV Community by Boban Mitevski (@bobanmit).</description>
    <link>https://dev.to/bobanmit</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4220969%2F1b0059e0-b657-4a44-ad2f-1017b974a0c2.png</url>
      <title>DEV Community: Boban Mitevski</title>
      <link>https://dev.to/bobanmit</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bobanmit"/>
    <language>en</language>
    <item>
      <title>Your XRechnung validator says "valid". Here's what that doesn't mean.</title>
      <dc:creator>Boban Mitevski</dc:creator>
      <pubDate>Mon, 02 Nov 2026 07:49:40 +0000</pubDate>
      <link>https://dev.to/bobanmit/your-xrechnung-validator-says-valid-heres-what-that-doesnt-mean-3o20</link>
      <guid>https://dev.to/bobanmit/your-xrechnung-validator-says-valid-heres-what-that-doesnt-mean-3o20</guid>
      <description>&lt;p&gt;From January 2027, German companies with more than ‚Ç¨800,000 turnover must send their business customers e-invoices (XRechnung or ZUGFeRD); from 2028, everyone must. So a lot of invoicing software is getting an XRechnung export right now, usually followed by a validator in CI. When it turns green, the job looks done.&lt;/p&gt;

&lt;p&gt;I build a generator and validator for these formats, and in the past few weeks I kept running into cases where "valid" meant less than people assumed. Here are six, each reproduced on official artefacts: the KoSIT XRechnung 3.0.2 test suite (2026-08-31) and the Peppol BIS Billing 3.0.21 rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. A validator only sees what's there
&lt;/h2&gt;

&lt;p&gt;Take the official KoSIT test invoice &lt;code&gt;01.01a-INVOICE_ubl.xml&lt;/code&gt;. Its buyer name is, literally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;cbc:RegistrationName&amp;gt;&lt;/span&gt;[Buyer name]&lt;span class="nt"&gt;&amp;lt;/cbc:RegistrationName&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's a test invoice, so that's fine. But it shows the point: the rules check that a buyer name &lt;em&gt;exists&lt;/em&gt;, not that it's a name. I took the same invoice and changed only the buyer's name and street:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Buyer name and street&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;left empty&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;rejected&lt;/strong&gt;: BR-07 "An Invoice shall contain the Buyer name (BT-44)", PEPPOL-EN16931-R008 "Document MUST not contain empty elements"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;not set&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;valid&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;[Buyer name]&lt;/code&gt; (original)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;valid&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I found exactly this pattern in an open-source invoicing tool this week: missing customer fields were filled with a &lt;code&gt;not set&lt;/code&gt; default, so an incomplete customer record produced an invoice every validator accepts.&lt;/p&gt;

&lt;p&gt;The same goes for tax categories. A small German business under the ¬ß19 UStG exemption (Kleinunternehmer) should mark its 0 % lines as &lt;code&gt;E&lt;/code&gt; (exempt, with an exemption reason). Another tool marks every 0 % line as &lt;code&gt;Z&lt;/code&gt; (zero-rated). That invoice passes the XRechnung rules without even a warning, and it's still the wrong category.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; never fill required fields with placeholders or defaults. Leave them empty so validation catches them, or better, refuse the export and tell the user which field is missing. And derive the VAT category from the tax situation, not from the rate.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Schema-valid isn't valid
&lt;/h2&gt;

&lt;p&gt;XSD validation checks structure: element names, order, data types. The business rules live in Schematron. I changed one number in the same test invoice, the total VAT amount (BT-110), from 22.04 to 23.04. The file is still perfectly schema-valid. The Schematron rules reject it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;BR-CO-14: Invoice total VAT amount (BT-110) = Œ£ VAT category tax amount (BT-117)&lt;/li&gt;
&lt;li&gt;BR-CO-15: Invoice total amount with VAT (BT-112) = total without VAT (BT-109) + total VAT (BT-110)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I've seen more than one project whose "validator" is well-formedness plus XSD. That catches typos in element names and very little else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; if your check doesn't run the EN 16931 Schematron (and for XRechnung the BR-DE rules), it isn't validating the invoice, only the XML.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The Schematron files alone don't reproduce KoSIT's verdict
&lt;/h2&gt;

&lt;p&gt;For XRechnung, the reference is the KoSIT validator with its XRechnung configuration (&lt;code&gt;validator-configuration-xrechnung&lt;/code&gt;). That configuration doesn't just bundle the Schematron files: its scenarios re-grade some rules. Comparing plain rule execution with KoSIT 1.6.3 and the 2026-08-31 configuration, we found for example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;UBL-CR-646&lt;/strong&gt; (sub invoice lines) is a warning in the plain rules, an &lt;strong&gt;error&lt;/strong&gt; in KoSIT.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CII-SR-465 / CII-SR-466&lt;/strong&gt; (person name &lt;em&gt;and&lt;/em&gt; department name in one contact) are &lt;strong&gt;errors&lt;/strong&gt; in KoSIT.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The second one has real consequences: a popular .NET library's XRechnung CII output writes both &lt;code&gt;PersonName&lt;/code&gt; and &lt;code&gt;DepartmentName&lt;/code&gt; when you set a contact with a department, and the current KoSIT configuration rejects those invoices (&lt;a href="https://github.com/stephanstapel/ZUGFeRD-csharp/discussions/986" rel="noopener noreferrer"&gt;ZUGFeRD-csharp discussion #986&lt;/a&gt;; the workaround is to set only one of the two).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; if you run the Schematron files yourself instead of KoSIT, keep a small severity-override table keyed by rule ID, and re-check it at every KoSIT configuration release. With those overrides, our results matched KoSIT on all 80 standard and technical-case instances of the official test suite.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Which rule version is your validator running?
&lt;/h2&gt;

&lt;p&gt;Mustang is a popular, well-maintained Java toolkit for ZUGFeRD, and it can validate XRechnung too. But its released versions (up to 2.26.0) still bundle XRechnung Schematron 2.4.0 from July 2025, while KoSIT has been on 2.6.0 since 2026-08-31. An update is in progress (&lt;a href="https://github.com/ZUGFeRD/mustangproject/issues/1274" rel="noopener noreferrer"&gt;mustangproject#1274&lt;/a&gt;). Until then, "Mustang says OK" isn't quite "KoSIT says OK" for XRechnung.&lt;/p&gt;

&lt;p&gt;That's not a dig at Mustang. Every validator lags at some point, because rules get bugfix releases twice a year. The problem is not knowing which version you ran.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; record the rule-set name and version in every validation report you store, and pin the versions in CI so an upgrade is a reviewed change.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Warnings are not failures
&lt;/h2&gt;

&lt;p&gt;The same official test invoice &lt;code&gt;01.01a&lt;/code&gt; comes back with a warning:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;BR-DE-TMP-32: Eine Rechnung sollte zur Angabe des Liefer-/Leistungsdatums entweder BT-72 "Actual delivery&lt;br&gt;
date", BG-14 "Invoicing period" oder in jeder Rechnungsposition BG-26 "Invoice line period" enthalten.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It's still a valid invoice. KoSIT marks it acceptable. If your pipeline (or your "e-invoice OK" tag on incoming documents) treats every warning as a failure, you'll reject correct invoices.&lt;/p&gt;

&lt;p&gt;Note the "entweder ‚Ä¶ oder": the rule is satisfied by &lt;em&gt;one&lt;/em&gt; of the three options. A single delivery date or invoicing period at document level is enough; you don't need a date on every line.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; map only errors (&lt;code&gt;flag="fatal"&lt;/code&gt; in the Schematron) to "invalid", and show warnings separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Pin what isn't versioned
&lt;/h2&gt;

&lt;p&gt;For Peppol BIS Billing, the authoritative rules are the Schematron files published on &lt;code&gt;docs.peppol.eu/poacc/billing/3.0/files/&lt;/code&gt;. Two traps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The URLs contain &lt;strong&gt;no version&lt;/strong&gt;: each release overwrites the same files. A CI job that downloads them silently changes when Peppol releases. Pin them by SHA-256 so a new release fails your build until you've looked at it.&lt;/li&gt;
&lt;li&gt;"3.0.21" is two different files. The GitHub repository (OpenPeppol/peppol-bis-invoice-3) got a &lt;code&gt;3.0.21&lt;/code&gt; release on 2026-10-28 (note: no &lt;code&gt;v&lt;/code&gt;, unlike &lt;code&gt;v3.0.20&lt;/code&gt;, so a &lt;code&gt;v*&lt;/code&gt; tag filter misses it). Its &lt;code&gt;PEPPOL-EN16931-UBL.sch&lt;/code&gt; is not the file on docs.peppol.eu: it contains a hotfix to rule PEPPOL-EN16931-P0112 (invoice type code 389 only between German parties, 384 also between Slovak ones). Same version number, different verdicts. The checksum is the only reliable name.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Required fields are never filled with placeholders or defaults; missing data stops the export.&lt;/li&gt;
&lt;li&gt;[ ] Validation runs the Schematron rules, not just the XSD.&lt;/li&gt;
&lt;li&gt;[ ] For XRechnung: KoSIT's configuration, or your own severity overrides that match it.&lt;/li&gt;
&lt;li&gt;[ ] Every stored report names the rule set and its version.&lt;/li&gt;
&lt;li&gt;[ ] Only errors fail; warnings are shown, not counted as failures.&lt;/li&gt;
&lt;li&gt;[ ] Downloaded rule files are pinned by checksum.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;To reproduce any of this: KoSIT's validator (Java, Apache-2.0) with &lt;code&gt;validator-configuration-xrechnung&lt;/code&gt; is the reference for XRechnung, and Mustang covers ZUGFeRD PDFs. If you'd rather not install Java, the validator I build runs the XRechnung 3.0.2, Peppol BIS 3.0.21 and ZUGFeRD / Factur-X rules (PDFs included) over HTTP, free for test files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://valinvo.bmk-hub.com/v1/validate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'X-Accept-Terms: 1.0'&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/xml'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-binary&lt;/span&gt; @invoice.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"ruleSet"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"xrechnung-3.0.2-ubl-invoice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"valid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"errors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"warnings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"rule"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BR-DE-TMP-32"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;}]}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every finding comes with its rule ID and XPath location. There's also a web version at &lt;a href="https://valinvo.bmk-hub.com" rel="noopener noreferrer"&gt;valinvo.bmk-hub.com&lt;/a&gt;, and a GitHub Action (&lt;a href="https://github.com/marketplace/actions/valinvo-e-invoice-validator-xrechnung-zugferd-peppol" rel="noopener noreferrer"&gt;valinvo-validate-action&lt;/a&gt;) for CI.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I build Valinvo. All results above were run on 2026-10-29 and re-checked on 2026-11-01 against the official KoSIT test suite (2026-08-31) and the Peppol BIS 3.0.21 artefacts from docs.peppol.eu. I wrote this with the help of an AI assistant; the tests and facts were checked against the official files.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>einvoicing</category>
      <category>testing</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
