Shipmind Labs

A validator that stays quiet about its blind spots is lying by omission.

We ran into this while building invoicerules, our open-source EN 16931 rule engine. The payment checks show it most clearly.

For each payment means entry, the rules do not all fire at once. They are gated on what the instruction itself declares.

BR-49 fails when the type code is blank, with no conditions attached.

BR-50 runs only when a credit transfer block is present, and it fails when the account identifier there is blank. No block, no finding.

BR-61 runs only when the trimmed code is one of three credit transfer codes (30, 31, 58), and then it demands an account identifier. So code 30 with no credit transfer block at all fails BR-61 on its own. BR-50 stays quiet, correctly, because there is nothing for it to inspect.

That gating matters. A global "strict mode" would have produced two findings for one defect, and taught people to ignore both.

The IBAN check follows the same discipline. When an account is present and the compacted value matches IBAN shape, we verify the mod-97 checksum: move the first four characters to the end, map letters to digits, reduce digit by digit, refuse anything outside 15 to 34 characters. A mismatch gets a specific code. An identifier that is not in IBAN shape is left alone, not approved, just not judged. Domestic account formats are not our business.

Then the part we think is underrated: we export UNCHECKED_PAYMENT_RULES. Five entries, each with the rule, what it covers, and a note on why we skip it: BR-CO-25, BR-51, PEPPOL-EN16931-R061, UNCL4461, ISO 13616.

Not a paragraph in the README. A data structure the caller can read, print in a compliance report, or diff against the next release.

Green output from a validator is not "this invoice is valid." It is "none of the rules we implement found a problem." The gap between those two sentences is where audits go wrong, and the honest fix is to publish the gap.

If you ship validation to other teams, the thing worth checking is whether your list of known gaps lives in the docs or in the output.

Was this useful?

Building something similar?

or email hello@shipmindlabs.com