Our zero-VAT check reports BR-IC for category K, not BR-K. The mismatch is deliberate, and it is why the rule identifier comes out of a lookup instead of out of the category letter.
That pattern is easy to get wrong. You have VAT categories, you have rules per category, and the obvious move is string concatenation: take the letter, wrap it in BR-?-10, done. It compiles, it reads cleanly, and it quietly invents identifiers that nobody on the receiving side recognises.
EN 16931 names the intra-community rules BR-IC-01 through BR-IC-12. There is no BR-K-10. It is a plausible-looking string that no receiving platform will ever quote back to you in a rejection message.
So in invoicerules, our open-source project, the zero-VAT reason check reads from a ReadonlyMap of category to identifier prefix. Z maps to BR-Z, E to BR-E, AE to BR-AE, G to BR-G, O to BR-O, and K maps to BR-IC. For every VAT breakdown group the check looks the category up. A category that is absent runs none of that block, which is how a standard-rated group falls through to BR-S-05 instead of being force-fitted into a zero-VAT rule.
Two more decisions fell out of the same table.
The reason check runs first and it is the stricter of the two. It passes only when the exemption reason or its code has non-whitespace content, and it reports against the reason field rather than the group.
The rate check runs second, and only when the rate actually parses as a decimal. An unparsable rate is skipped here rather than refused, because a malformed number is a different failure from a wrong number, and reporting it as a zero-rate violation sends the sender hunting in the wrong place. When it does run, it demands a rate equal to zero and names the stated rate in the message.
The takeaway for practitioners is smaller than the standard. An error identifier is not an internal label. It is the one token that travels from your validator, through a rejection, into someone else's support ticket. Once it travels, it belongs to the vocabulary of the receiving platform, and your type system has no say in it. Derived identifiers are correct right up to the point where the standard stops being regular, and standards written by committees are rarely regular.
Which is probably why the map is worth the extra indirection. It makes the irregular case the same amount of work as the regular ones, and it means the identifiers listed in the README catalog are the identifiers that show up in coverage and in production errors.
If you maintain anything that emits standardised error codes, it is worth knowing whether yours are looked up or built from the data.