Shipmind Labs

A credit note has to name the invoice it corrects. An invoice has to name none. Those two sentences are the whole difference between the documents, and everything else is identical.

Most billing models get this wrong by splitting the types. You end up with an Invoice class and a CreditNote class that share ninety percent of their fields, drift apart over two years, and quietly disagree about rounding.

In euinvoice, our open-source European e-invoicing library, there is one record. DocumentKind has two members, INVOICE and CREDIT_NOTE. The invoice carries a kind that defaults to INVOICE and a corrects that defaults to None, so a document is an invoice unless it says otherwise. Amounts, regime, and the line checks against that regime are untouched by the distinction.

What makes it interesting is that the obligation runs both ways, and both directions are enforced.

Construction refuses a kind that is not a DocumentKind and a corrects that is not a string. Then it strips the string and stores None if nothing is left, so whitespace cannot pass as a reference. That one strip is what stops a well-meaning integration from satisfying a required field with an empty box.

The compliance layer then runs the correction check first, before the seller VAT id, reverse charge, and exemption checks. A credit note with an empty corrects gets a missing-field error citing the corrected-invoice reference under Art. 219. An invoice that does carry a corrects gets the mirrored error. Construction raises on any result, so neither an unnamed credit note nor an invoice claiming to correct something can be built at all.

Our opinion: the mirrored half is the one teams skip, and it is the one that saves the audit. Forgetting the reference on a credit note is a visible failure, since someone notices the unmatched document. An invoice that carries a stale correction reference is invisible. It validates, renders, transmits, and then one day a tax authority reads your ledger as two corrections of a document that was never corrected. Rules that only check the presence of a field let that through. Rules that check absence as deliberately as presence do not.

Downstream stays dumb on purpose. The document copies both fields, the title resolves through a label map, and the renderer prints the corrects line only when there is something to print. No presentation-layer logic deciding what kind of paper this is.

If you have built billing, you modelled corrections either as a separate document type or as a flag on the same record, and we suspect one of those two is the choice you ended up regretting.

Was this useful?

Building something similar?

or email hello@shipmindlabs.com