Credit Note De-allocation: Reversing Allocations and Reopening Credited Invoices
Why This Matters
A credit note that has been allocated against an invoice was, until now, a one-way act. You could apply a credit, but you could not take it back, and a fully-credited invoice was stuck closed. That left a gap in the correction lifecycle: the only ways to fix a mistaken allocation were heavy-handed, and a credit note could not be voided while it still carried live allocations.
This release closes that gap. Allocation becomes round-trippable. Reversing an allocation reopens the invoice to exactly the state its payments imply, frees the credit note to be voided, and leaves a complete record of who reversed what and why. The correction lifecycle (void, payment reversal, credit notes, and now de-allocation) is whole.
Credit Note De-allocation
Reversing an allocation
A new endpoint reverses a single live allocation of a credit note. You name the allocation by its own id, which the allocation response now returns, and supply a reason for the reversal.
-
The allocation is marked reversed, never deleted. The original amount and the audit trail survive. Who reversed it, when, and why are recorded together as an all-or-nothing requirement enforced at the database (migration 112, AX-CN-003). A reversal cannot be recorded half-written.
-
Reversed allocations stop counting immediately. A reversed allocation no longer consumes the credit note’s total or the invoice’s outstanding balance, enforced by a database trigger over the live allocation set (migration 112, AX-CN-003). This is what then allows the credit note to be voided.
-
Two layers of replay protection. The endpoint honours an Idempotency-Key for safe retries, and reversing an already-reversed allocation returns a conflict rather than acting twice.
Reopening a credited invoice
When you reverse the allocation that had driven an invoice to fully credited, the invoice reopens to its payment-derived status: partial if a payment still remains on it, otherwise sent. The number of credits still live changes only the outstanding balance, never the status label.
This reopening behaviour is covered directly by the de-allocation route test. The formal coherence proof that ties an invoice’s status to its payment state across arbitrary allocate and de-allocate sequences lands in a following release (see Known Issues).
A reverse edge for the invoice state machine
The invoice state machine now permits a credited invoice to reopen to sent or partial, enforced at the database (migration 116, AX-INV-001). Credited is no longer a dead end; it is a reversible state, in the same way a paid invoice became reversible when payment reversal shipped.
Reopening an invoice by setting its status directly is still refused. Reopening happens only as a derived consequence of de-allocation, and the public status endpoint says so when asked to do otherwise.
Operational Impact
- A mistaken credit allocation can be corrected without deleting records or rebuilding the invoice.
- A credit note can be voided once its allocations are reversed, completing the void-after-allocate path.
- A fully-credited invoice reopens to the exact status its payments imply, with no manual status editing.
- De-allocation records an identified user and a reason on every reversal. Because the database requires a recorded user, de-allocation is a user action: API-key callers receive a 403, an application-level safeguard over that database requirement.
- The allocation response now returns the allocation’s own id, so a caller can reverse a specific allocation later. This is additive; existing integrations are unaffected.
Versioning
The internal axiom registry advanced from 2.36 to 2.38 across two amendments: the invoice state machine gaining its reverse edge (AX-INV-001, ADR-017) and the allocation conservation axiom gaining its de-allocation runtime (AX-CN-003, ADR-018). The delivered mechanically-enforced invariant count is unchanged; both are amendments to existing axioms, not new ones.
No public API surface was removed or changed in a breaking way. One endpoint was added for reversal, and the allocation response gained one additive field.
Files Changed
Backend:
- Credit note service and routes
- De-allocation logic: the reversal, the live-balance recompute, and the invoice reopen
- The new reversal endpoint, with the actor guard and idempotency handling
- The allocation response now returns the allocation’s own id
- Invoice service
- The reopen-guidance message now points a credited invoice at de-allocation
- Kernel records
- Axiom registry advanced to 2.38; two architecture decision records added
- Tests
- A new route test for de-allocation (reopen-to-sent, reopen-to-partial, double-reverse conflict, unknown allocation, blank reason, API-key refusal, malformed id)
Frontend / Docs site:
- The API reference gains the de-allocation endpoint page, generated from the OpenAPI specification. No portal UI change.
Known Issues
- The mechanical coherence proof over de-allocation is owed in a following release. De-allocation already restores the payment-derived status by construction, and the route test asserts that target directly, so the behaviour is correct today. The property test that exercises it across arbitrary allocate and de-allocate sequences, and the registry amendment that records the broadened relation, are scheduled next.
De-allocation completes the credit-note correction lifecycle and sets up the credit-note void and read surfaces still to come.