Enforced invoice lifecycle
The invoice lifecycle the documentation describes and the lifecycle the kernel enforces are now the same object.
Enforced transition table. Invoice status changes now follow a strict table at both the service and the database: draft can become sent or cancelled; sent can become partial, paid, or written_off; partial can become paid or written_off. paid, cancelled, and written_off are terminal. Cancellation applies to drafts only — an issued invoice is a tax document and is closed via written_off instead. Illegal transitions, including setting an invoice to its current status, return a 400 naming the rejected pair.
Status endpoint surface. The status endpoint now accepts sent, cancelled, and written_off. paid and partial are payment-derived — they are set automatically by the payment endpoint and can no longer be set by hand, so an invoice’s status and its recorded payments always agree. A new credited status enters the vocabulary, reserved for the forthcoming credit-note mechanism.
Payment guards. Payments now apply only to issued, open invoices. Paying a draft returns a 400 telling you to issue the invoice first; paying a terminal invoice returns 400; overpayment remains rejected as before.
Honest 404s. Operations against a missing invoice (status change, edit, delete, payment) now return the 404 the documentation has always declared, instead of a silent success.
First-issue timestamp. sentAt now records the first issue of an invoice and is never restamped by re-sends or later transitions. Existing issued invoices missing the timestamp have been backfilled from their audit history.
Cancelled invoices cannot be emailed. Emailing a cancelled invoice returns a 400 before any email is sent.
This release tightens the API contract ahead of launch: the status enum shrink and the payment guards are stricter than the previous behaviour, and pre-launch is the moment to make them so. Requests that previously relied on transitions outside the table will now receive a 400 with the exact reason.