v5.34.0 28 May 2026 Fix Improvement

Invoice Payment Scale Fix and AX-ERR-001 Activation

Why This Matters

A defect in the payment-recording route caused recorded payment amounts to be stored at one one-hundredth of the value submitted. The corrupt value sat below the magnitude bound (a £120 payment stored as 120 pence rather than 12000), so the existing payment-bound constraint had no signal to fire on. A bound constraint cannot catch a scale error that lands below the ceiling: 120 is a lawful pence value in isolation; only its relationship to intent makes it wrong.

The fix is a single deletion at the route boundary, but the durable backstop is an executable test that asserts wire-pence-in equals stored-pence-out on the payment path. Any future edit reintroducing a unit conversion is caught by CI before it can corrupt a row. Magnitude bounds and scale identity are two different invariants and now have two different enforcement mechanisms.

Verification of the fix surfaced a second, larger architectural finding: the global error handler in the API server was overriding every per-route error mapping by string-matching on “violates” in the message and returning a generic DATABASE_CONSTRAINT envelope. The AX-ERR-001 infrastructure designed to do this properly (typed ApiError class, CONSTRAINT_ERROR_MAP, toApiError pipeline) was complete, CI-verified for coverage, and never imported into the server. This release activates that pipeline. Every database constraint violation across the API now routes through the authoritative map, yielding stable typed error codes and accurate user-facing messages.


Fix

  • Recorded payment amounts now store at the correct scale. The invoice payment endpoint previously applied an unnecessary unit conversion at the route boundary, causing payment values to be stored one hundred times smaller than the user submitted. The conversion has been removed; the validated wire value is passed straight to the storage layer, matching the contract that the wire and the column both speak the same units. Invoices that receive the correct full payment amount now transition to paid.

  • Over-total payments return a typed 4xx with an accurate message. An attempt to record a payment exceeding the invoice total is user input, not a server fault. The database-level magnitude constraint already rejected such payments at write time; the response is now a 400 validation_error with the message “Payment amount exceeds document balance”, sourced from the authoritative constraint-to-error map.

Scale-identity backstop

  • Property test on the payment boundary. An executable invariant test now asserts that for any pence integer the schema accepts, the value reaching the storage layer equals the wire value exactly. The test includes a regression lock for the reported scenario and a structural assertion that the relevant service function contains no unit-conversion calls. Any future edit reintroducing such a call breaks the test in CI.

Improvement

  • AX-ERR-001 deterministic constraint→error pipeline activated. The global error handler in the API server now uses the typed ApiError class and the CONSTRAINT_ERROR_MAP lookup that have existed in the codebase since v5.17.1. Two new branches at the top of the handler emit ApiError instances verbatim, and route database errors with a named constraint through the map. The previous generic DATABASE_CONSTRAINT branch remains as a secondary safety net for errors without a constraint name; in practice it should rarely fire. Every endpoint that throws a typed error or surfaces a named constraint violation now produces stable, accurate responses without per-route duplication of the mapping.

Operational Impact

  • Recorded payments store at the wire scale; no pounds-to-pence drift at the route boundary
  • Over-total payment attempts return 400 validation_error with an accurate user-facing message, sourced from the constraint map
  • Every API endpoint that surfaces a named database constraint violation now produces a typed response; per-route error mapping is no longer overridden by the global handler
  • The scale invariant on the payment input is mechanically enforced by an executable test, complementing the database-level magnitude bound

Proof Anchors

  • Scale identity: api/test/invoice-payment-scale-identity.test.ts (Class M, executable invariant verified in CI; AX-MON-001 Runtime proof anchor for the payment input boundary).
  • Magnitude bound: chk_payment_bound (migration 071, unchanged).
  • Constraint-to-error mapping: CONSTRAINT_ERROR_MAP in api/src/lib/api-error.ts (AX-ERR-001; coverage verified by scripts/constraint-coverage-check.sh).
  • Pipeline activation: setErrorHandler in api/src/server.ts now imports and uses ApiError and toApiError.

Files Changed

Backend:

  • api/src/routes/invoices.ts
    • Payment-record handler: unit conversion removed; validated pence integer passes straight to the service.
    • Dead route-level 23514 handler removed; constraint mapping lives in CONSTRAINT_ERROR_MAP, not in the route.
  • api/src/services/invoice-service.ts
    • recordPayment parameter renamed to make the wire contract explicit; JSDoc rewritten to document pence end-to-end.
  • api/src/server.ts
    • setErrorHandler now uses ApiError and toApiError via two new branches at the top of the handler.
  • api/test/invoice-payment-scale-identity.test.ts
    • New executable invariant test for the payment boundary.
  • api/db/migrations/088-item31-payment-scale-reset.sql and its dry-run twin
    • Enveloped reset for two test invoices that held corrupt values from the defect’s pre-fix window.
  • api/db/migrations/087-quote-domain-separation.sql and its dry-run twin
    • Migration-determinism markers added to two DEFAULT now() column defaults flagged by the AX-MIG-002 deploy gate (per-row at insert, not data backfill).
  • api/src/services/quote-service.ts
    • // wire:bigint annotations added to three .toNumber() calls flagged by the AX-MON-003 deploy gate; target columns confirmed as accounting.pence BIGINT.
  • docs/kernel/axioms.yml
    • Registry v2.4 → v2.5. AX-MON-001 gains a new delivered runtime proof anchor for the payment boundary specifically; status and currentLayers unchanged because broad Runtime delivery is not yet complete.

Frontend:

  • No production frontend changes. The existing payment UI was already sending pence correctly; the fault was entirely in the API route.

Impact

None on existing correct payments. Two test invoices held corrupt values from the defect’s pre-fix window; both were reset to the pre-payment state by migration 088 and re-exercised against the fixed route during release verification. Both landed correctly: full payment of £120 transitioned the invoice to paid (amount_paid stored as 12000), partial payment of £50 left the invoice partial (amount_paid stored as 5000), and an over-total attempt was rejected with the new typed 400 envelope.

The AX-ERR-001 activation changes response shapes for any endpoint that previously fell through to the generic DATABASE_CONSTRAINT envelope. The new shape is { code: <typed code>, message: <authoritative message>, field: <field name if mapped> } for constraints in the map, and the generic shape for the residual. Clients that match on the previous generic shape may need to adapt.