Credit-Note Allocation Endpoint, the Credited-Flip Wired
Why This Matters
A credit note only does its job once it can be applied to an invoice. Allocation is that act: it draws an issued note down against an open invoice and, when the invoice is fully offset, moves it to credited. This release wires that act to the API. The rule that bounds it has been enforced at the database since the credit-note schema landed; what changes here is that the rule now governs a live endpoint rather than a dormant service method.
The order follows the kernel discipline. The enforcement floor went in first, the database refuses an over-allocation, and only now is the act exposed on top of it. The endpoint cannot drift the ledger, because the guarantee it relies on is not its own to keep.
Credit-Note Allocation
Applying an issued note to an invoice
-
Allocation is now an API operation. An issued credit note can be allocated against an open invoice for an amount in pence. When the allocation drives the invoice’s outstanding balance to zero, the invoice moves to credited; a partial allocation leaves it open for further allocation or payment. Allocation is permitted only from an issued note against an open invoice. Any other state is refused, and a missing note or invoice is reported as not found.
Proof anchor: API endpoint, OpenAPI reference at docs.speybooks.com.
Conservation holds (AX-CN-003)
-
A credit note cannot over-apply. The live allocations of a note can never exceed its own total, and the live allocations against an invoice can never exceed that invoice’s outstanding balance. Two allocations racing the same invoice are serialised so they cannot both pass and over-apply. This is enforced at the database, so the endpoint inherits the guarantee rather than re-implementing it.
Proof anchor: database trigger, AX-CN-003 (migrations 110, 112).
The credited-flip at runtime (registry 2.36)
-
The flip is now a recorded runtime behaviour, not a dormant one. The service recomputes the invoice’s remaining balance over the live, non-reversed allocation set and moves the invoice to credited at exactly zero. That computation runs in arbitrary-precision integer arithmetic, so an invoice balance beyond the safe range of a floating-point number is never rounded, and the remaining balance is returned on the wire as a string in pence for the same reason. The conservation axiom now records this runtime layer alongside its existing database and test layers.
Proof anchor: runtime service plus API route, AX-CN-003 runtime layer; axiom registry 2.36.
Executable Proof Anchors
-
The isolation the conservation race depends on is pinned by a test. The serialisation that stops two allocations over-applying relies on a specific transaction isolation level. A test asserts that the allocate path runs at that level, so a future change that weakened it would fail the build rather than silently break the race protection.
Proof anchor: executable invariant test, AX-CN-003.
-
The endpoint behaviour is pinned end to end. Tests drive the endpoint itself: a full allocation flips the invoice to credited, a partial leaves it open, and each precondition returns the correct refusal. The remaining-balance field is asserted as an exact string, so the precision guarantee is held at the surface as well as in the service.
Proof anchor: executable route test.
Operational Impact
- An issued credit note can be allocated against an open invoice over the API, with the invoice moving to credited when fully offset.
- An over-allocation is refused at the database, whether it exceeds the note’s own total or the invoice’s outstanding balance, and concurrent allocations against one invoice cannot both pass.
- The remaining-balance figure is exact at any size; no invoice balance is rounded by a floating-point wire type.
- The allocation request is idempotent under an Idempotency-Key, like a recorded payment.
- No existing endpoint or stored value changed. The API surface grows by one endpoint, to 203.
Versioning
The axiom registry advanced to 2.36. The change records the allocation-conservation axiom (AX-CN-003) gaining its runtime layer, now that the allocate act is wired; no new axiom was minted, and the count of mechanically enforced invariants is unchanged. The public API surface grew by one endpoint, from 202 to 203.
No breaking change. No existing endpoint was altered.
Files Changed
Backend:
- The API gained a credit-note allocation endpoint.
- The credit-note resource gained its API reference documentation.
- The axiom registry recorded the allocation-conservation axiom’s runtime layer.
- The test suite gained an end-to-end test for the allocation endpoint, including the isolation pin.
Frontend / Docs site: The credit-note allocation endpoint is published to the API reference.
Known Issues
- De-allocation is not yet exposed. An allocation can be applied but not yet reversed over the API; the reversal endpoint lands in the next release and completes the correction surface. Until it does, a credit note that carries a live allocation cannot be voided, because voiding requires its allocations to be cleared first.
The next release wires de-allocation, the inverse act that clears an allocation and re-opens the path to voiding a credit note.