Sent invoices can't be edited — full stop, not even to fix a typo
// CONTEXT
Designing the Invoice/Quote lifecycle FSM: which statuses allow their lines, discounts, or metadata to still be changed.
// WHAT THE OBVIOUS VERSION WOULD DO
Keep sent invoices editable but wrap every change in an audit trail / version history. That's the standard reflex for "important record that occasionally changes": make it mutable, log who touched what. For a legal document it's the wrong default — mutable-with-history still lets the row drift from the PDF the client already holds.
// THE CODE THAT DID IT
src/ledgercore/domain/documents/state_machine.py:
def assert_invoice_editable(invoice: Invoice) -> None:
if invoice.is_frozen:
raise ImmutableDocument(
f"Invoice {invoice.id} is in status '{invoice.status.value}' and is "
"immutable. Issue a CreditNote to correct errors (§2.2)."
)
// WHAT I FOUND
Once an invoice leaves DRAFT (i.e. it's been sent to a client), it's a legal document — mutating it after the fact would mean the PDF a client already has and the DB row could silently diverge, and in French invoicing law a sent invoice isn't supposed to be edited at all. So the domain makes it structurally impossible: any status other than DRAFT raises ImmutableDocument, and the only sanctioned way to correct an error is a separate CreditNote entity that references the original rather than mutating it.
// LIMIT
The guard lives entirely in the domain layer (assert_invoice_editable) — verified there's no backing constraint at the DB level: status is a plain String(20) column (models.py:62), no CheckConstraint, no trigger, and there isn't even a migrations system (no Alembic dir — schema comes from Base.metadata.create_all()). If any code path ever saves an invoice without going through the use case that calls the guard, nothing in the database stops it.
// TAKEAWAY
When a discount is applied after per-line tax has already been computed, re-deriving the tax from the discounted base (not just subtracting from the final total) is the only way to keep the tax base legally correct.