Add backend-authoritative invoice draft editing (ADR 0006/0007)

Move the invoice draft editor to a backend single source of truth: an
in-memory InvoiceDraftSession (per-token, cached) holds the editable payload,
server-computed sums/VAT and validation, plus an automatic change history. The
browser posts single edits; the server recomputes and signals the editing
session over a dedicated SignalR hub (DraftPreviewHub) to re-fetch.

This reverses the previously-documented stateless editor (EVAL_live_invoice_editing,
INVOICE_LIFECYCLE §10), by explicit product decision — captured in ADR 0006 and 0007
plus the live-draft-editing concept doc.

Backend (this milestone):
- InvoiceDraftSession + ChangeHistoryEntry data holders
- InvoiceDraftCalculator: pure port of quantChange/invSumUpdate (§13b, VAT-by-rate)
  and consistency checks — fully unit-tested
- IInvoiceDraftCache/InvoiceDraftCache: in-memory store with idle sliding TTL
- IInvoiceDraftService/InvoiceDraftEditService: open (payload or DB reload), patch,
  build state, flush via existing RegisterInvoiceAsync (no new persistence), preview
  from cache, discard (DB reload), history
- InvoiceDraftExpiryService: pre-expiry warning + eviction-with-reason
- DraftPreviewHub + IDraftNotifier/DraftNotifier: targeted draftReady/draftExpiring/
  draftClosed signals per draft token
- inv/dopen|dstate|dpatch|dpreview|dsave|dhistory|ddiscard|dclose endpoints; save
  reports success/failure via the existing EventService
- DI + hub mapping in Program.cs

Frontend (additive foundation): $fis.draft SignalR client for /draftpreview.
The editor DOM inversion (routing deltas, rendering from server state) is the
next, separately-verified step; existing endpoints are unaffected.

Tests: 30 new (calculator, cache, expiry, patch/history, flush); 306 total passing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Stefan
2026-07-10 13:29:35 +02:00
co-authored by Claude Opus 4.8
parent e53d8962ad
commit af445c015e
26 changed files with 2121 additions and 6 deletions
+12 -3
View File
@@ -337,9 +337,18 @@ flowchart TD
## 10. Key invariants worth remembering
- **Stateless editor**: every preview/save/finalise call re-posts the full
`invc` JSON; the server never holds a partial invoice in memory or session
between requests (see `EVAL_live_invoice_editing.md`).
> **⚠️ Updated (2026-07-10):** the "stateless editor" invariant below describes the
> **legacy** draft-editing flow. Invoice draft editing is being moved to a
> **backend-authoritative** model where the server holds the draft in an in-memory
> cache (the single source of truth), the browser posts single edits and re-fetches on
> a SignalR signal, and totals are computed server-side. See ADR
> [`Decisions/0006-backend-authoritative-draft-editing.md`](Decisions/0006-backend-authoritative-draft-editing.md)
> and [`Concepts/live-draft-editing.md`](Concepts/live-draft-editing.md). Finalise/email
> (§5–§6) are unchanged. The remaining invariants below still hold.
- **Stateless editor** *(legacy — see the note above; superseded by ADR 0006)*: every
preview/save/finalise call re-posts the full `invc` JSON; the server never holds a
partial invoice in memory or session between requests (see `EVAL_live_invoice_editing.md`).
- **Totals come from the registration, not the rendered lines**: `sms.ttn`
/`sms.ttb` (posted) become `InvoiceBalance`/`InvoiceBalance_net`; display
mode (set pricing) never changes what the customer owes.