v5.33.0 28 May 2026 Feature Improvement Fix

Quote Domain Runtime Layer: Service, Route, Atomic Convert

Why This Matters

The previous release (5.32.0) shipped the schema half of the quote domain rebuild: new tables for quotes and quote lines, the invoice constraint surface stripped of quote accommodations, bidirectional provenance wired at the schema level. The Known Issues section of that release was honest about what remained owed: the quote API endpoints still threw on writes, no service layer existed, no atomic convert function existed, and the provenance invariant was representable but not yet runtime-enforced.

This release closes those Known Issues. Quotes can now be created, updated, status-transitioned, converted to invoices, and deleted through the API. Every mutation writes a hash-chained audit record. The quote-to-invoice conversion is a single atomic transaction that either fully completes or fully rolls back, and the bidirectional provenance between converted quote and spawned invoice is now mechanically verifiable rather than an operational expectation.

The session also surfaced a class of latent read-boundary defects that were waiting in both quote and invoice paths: database driver coercion of dates, timestamps, and small-scale numeric values varies by environment and driver build, and the previous schemas declared one shape and would have failed in the other. Production quote reads exposed this immediately; production invoice reads had been getting away with it because the affected emission paths were exercised less frequently. The shared read-boundary primitives now make this class of defect unrepresentable across both domains. That is the side benefit; the headline is that the quote domain rebuild is complete.


Quote Service Introduced

The quote domain now has a dedicated service layer that owns calculation, transactions, audit writes, and the state machine. The route layer carries only dispatch, validation, and response shaping. No SQL, no transactions, no business logic, and no audit writes appear at the route layer.

Calculation Boundary

Quote line arithmetic runs through the universal calculation template that governs the invoice domain: quantities and unit prices cross the wire as exact decimal strings, VAT rates as integer percentages, totals as pence integers. Decimal.js carries every intermediate value; floats do not appear on any path between the ingestion boundary and the database boundary. The single rounding authority (accounting.round_half_up) is used by both the service arithmetic and the database CHECK constraints, which means service and database agree on every penny by construction rather than by coincidence.

Proof anchors: chk_quote_lines_net_pence_correct, chk_quote_lines_vat_pence_correct, chk_quote_lines_gross_pence_correct (migration 087, Class M). Each constraint independently verifies the service computation for that column.

Header to Line Reconciliation

The quote header subtotal, VAT amount, and total are pinned to the sum of their lines by a header-level CHECK constraint. A header that disagrees with its lines cannot exist in the database.

Proof anchor: chk_quotes_totals_correct (migration 087, Class M).

State Machine

The quote lifecycle has six states and seven legal transitions. The public status-update endpoint admits four targets (draft, sent, accepted, declined); converted is reachable only through the dedicated convert endpoint, and expired is reserved for a future expiry automation. The service rejects illegal transitions with a typed allowlist; the database CHECK rejects them again as a second layer.

Proof anchors: service-level assertTransitionAllowed, chk_quotes_status (migration 087, Class M).

Audit Chain Integration

Every quote mutation now appends a hash-chained record to the canonical audit trail: creation, update, status change, conversion, and deletion. Conversion appends two records in one transaction (one for the source quote, one for the spawned invoice) so the chain captures both halves of the morphism. No quote mutation writes to the legacy audit log; the chain advances monotonically.

Proof anchor: accounting.audit_trail hash-chain head update via writeAuditRecord (Class M via chain integrity check on insert).


Atomic Quote-to-Invoice Conversion

The convert endpoint is the centrepiece of the rebuild. It locks the source quote, verifies the status guard and the empty-quote guard and the line account-assignment guard, issues an invoice number atomically, inserts the spawned invoice with both foreign keys set, copies the lines verbatim, updates the source quote to converted status with the back-reference foreign key, and writes a dual audit record. The whole sequence runs inside one transaction. Any failure rolls everything back; partial conversions are not representable.

The bidirectional provenance invariant from 5.32.0 is now mechanically enforced at runtime. The integrity-harness reconciliation query (every converted quote and its spawned invoice must agree mutually on the foreign key pair) returns zero rows by construction. This is the Class M promotion the previous release flagged as pending.

A second conversion attempt against an already-converted quote returns a conflict response rather than spawning a duplicate invoice. Idempotency on retry holds.

Quotes whose lines have no account assignment cannot be converted (the resulting invoice would be unbookable). The guard runs at the service layer with a clear error before any database mutation.

Proof anchor: bidirectional FK pair on accounting.quotes and accounting.invoices (migration 087 schema, runtime enforcement this release, Class M).


Read-Boundary Hardening

The read boundary between the database driver and the service layer now normalises driver-specific coercions to canonical forms at parse time. This closes a class of defect that affected both the quote and the invoice domains.

The PostgreSQL driver coerces DATE and TIMESTAMPTZ columns to native Date objects under its default configuration and ISO strings under certain alternative configurations. The previous read schemas declared one shape and would have failed under the other. The new preprocessors accept either and canonicalise to a string before validation, so downstream code no longer has to branch on driver behaviour.

Small-scale NUMERIC columns (VAT rates and similar) exhibit the same duality: returned as strings under some driver builds and as numbers under others. A shared preprocessor coerces both to a bounded number at the parse boundary.

Both fixes apply to the invoice domain as a side benefit. The invoice read path carried the same latent coercion mismatches; they had not surfaced because the affected emission paths were exercised less frequently in production traffic. The shared primitives now make this class of defect unrepresentable across both domains.


Foreign Key Errors Now Return 400, Not 500

When a request references a contact, account, or other resource that does not exist in the caller’s tenant, the API now returns a 400 validation error with the referenced subject named in the message (“Referenced contact does not exist”). Previously the foreign key violation surfaced as a 500.

The mapper extracts the subject from the constraint name at the route’s error boundary. Check-constraint violations and not-null violations are deliberately left as 500 because those indicate a service/schema mismatch rather than bad user input, and the journal needs the full stack for diagnosis.


Operational Impact

  • Quote API endpoints now function. Create, update, status transition, convert, delete are all operational. The Known Issue from 5.32.0 (“quote API endpoints still throw on writes”) is closed.

  • Provenance invariant promoted from operational to Class M. The Known Issue from 5.32.0 (“provenance invariant not yet runtime-enforced”) is closed; the convert function writes both foreign keys in one transaction, and the reconciliation query returns zero rows by construction.

  • Audit chain advances on every quote mutation, including the dual write across both entities during conversion. The chain captures the full lifecycle from creation through conversion in a single monotonically-advancing sequence.

  • Foreign key violations against user-supplied identifiers return 400 validation errors with a useful subject rather than 500.

  • Database driver coercion differences between environments no longer affect parse boundaries. The shared read-boundary primitives apply to both quote and invoice schemas.

  • Illegal quote status transitions are rejected at the service layer and again at the database layer. Two-layer enforcement.

  • End-to-end verification ran against the live system as part of this release. One quote was created (QT-0001), status-transitioned through sent and accepted, and converted to a draft sales invoice (INV-0012). The bidirectional provenance reconciliation query returned zero rows; the audit chain advanced from QUOTE_CREATED through QUOTE_CONVERTED and INVOICE_CREATED_FROM_QUOTE with every chain hash at full length.


Known Issues

  • Outer error envelope mismatch. The route layer correctly constructs typed validation_error and conflict responses for the foreign-key and idempotency cases; the outer Fastify error handler currently re-wraps some of these as a generic envelope before they reach the client. Service-layer logging captures the typed errors correctly. Alignment of the outer envelope to the inner typed code is a follow-up; no functional impact, journal traces are accurate.

  • Invoice audit-log legacy path. The invoice service still writes mutation records to the legacy audit log table rather than the canonical audit chain that the quote service now uses. The invoice domain’s migration to the canonical chain is owed; it was not part of the quote rebuild scope. Tracked.

  • Quote terms field silently dropped. The quote create endpoint accepts a terms field in the request body for forward compatibility, but the schema does not yet carry a terms column. The field is dropped silently on write. A future single-purpose change will add the column and wire the field end-to-end. This matches the pre-rebuild behaviour and is intentionally preserved rather than rushed in.


Files Changed

Backend:

  • Quote service introduced. Owns calculation, transactions, audit writes, the typed state machine, and the atomic convert morphism. Built on the same calculation contract as the invoice service.

  • Quote routes rewritten on the layered contract. Dispatch, validation, prefixed-ID resolution, and response shaping only. No SQL, no transactions, no audit writes, no business logic. The PostgreSQL error mapper that promotes foreign-key violations to 400 validation errors lives at the route’s error boundary.

  • Quote domain types added to the shared type registry, including the wire-facing Zod schemas with string-decimal monetary fields and the service-facing input shapes.

  • Read-boundary primitives hardened in the shared schema library. Date and timestamp preprocessors normalise driver coercion variance; a NUMERIC fractional preprocessor handles string-or-number driver behaviour. Applied to both quote and invoice read schemas.


With this release the quote domain rebuild is complete. The agreement-to-invoice path is now mechanically continuous from the first line entered to the spawned invoice’s first ledger posting, with audit and provenance verified at every step.