Shipmind Labs

One field in the model. Two separate elements in the output. And a warning for the case where there is nowhere to put it at all.

That is the shape of a due date in e-invoicing, and it is a good example of a mapping problem that looks trivial right up until you write it down.

The business term is a single date: when payment is expected. In our open-source project invoicerules it is one optional model field on the invoice. The output format, though, does not agree with itself about where that date lives. On an invoice it gets its own top-level element. On a credit note that element does not exist, so the date can only ride inside a payment instruction block.

So the same field has two destinations depending on the document type, and one of those destinations depends on other data being there.

The naive version is a branch at write time: if credit note, write here, else write there. That branch then gets copied into every place that touches the date: serialisation, validation, error paths.

We pushed it down instead. The syntax description for each document type carries an optional element name for the due date. The invoice syntax fills it in. The credit note syntax leaves it blank on purpose, and the path builder turns that absence into the nested payment-instruction path. The result is that the path lookup returns somewhere to write for both document types, and nothing upstream has to know which case it is in.

The second half is where we had to be honest. On a credit note the nesting target may not exist, and with no payment instructions the date has no container. That is not invalid. The document still passes. But the date gets silently dropped on the way out.

So that case warns rather than fails. Validation reports three different things with three different weights. A due date that is not a date fails and stops further checks. A due date before the issue date fails. A due date with nowhere to land warns, and says why.

The principle we keep coming back to is that a validator should distinguish between wrong, impossible and lossy. Wrong is rejected. Impossible is rejected. Lossy is reported, because the sender is the only one who can decide whether losing that value matters, and they can only decide that if someone tells them.

One more detail that costs nothing and saves support time: every rule carries an identifier prefixed with the library's own name. A receiving system should not be able to mistake our check for a standards body's rule and quote it back at the sender as if it were official.

If you build validators, the line between a hard failure and a warning is worth drawing on purpose rather than by accident. Silent data loss is the hardest category to place, we think, and the one people most often collapse into one of the other two.

Was this useful?

Building something similar?

or email hello@shipmindlabs.com