--- status: Active lastUpdated: 2026-07-10 applyTo: - "Fuchs/Services/InvoiceDraft*" - "Fuchs/Services/IInvoiceDraft*" - "Fuchs/code/InvoiceDraftSession.cs" - "Fuchs/code/InvoiceDraftCalculator.cs" - "Fuchs/Notifications/DraftPreviewHub.cs" - "Fuchs/Notifications/*DraftNotifier*" - "Fuchs/Controllers/IntranetController.InvoiceDraft.cs" - "Fuchs/js/intranet/**" relatedDecisions: - "0006-backend-authoritative-draft-editing.md" - "0007-targeted-draft-signalr-groups.md" --- # Live draft editing (backend-authoritative invoice previews) ## Summary While a back-office user edits an invoice draft, the authoritative state is held in server memory, not in the browser. The browser posts single edits, the server mutates the cached record, recomputes totals/VAT and re-validates, then pushes a "state changed" signal so the browser re-fetches and re-renders. This makes the backend the single source of truth (server-computed sums, consistency checks, in-place PDF preview, change history, explicit discard), reversing the earlier stateless editor. Invoices are the pilot; reminders are intended to mirror the same design. ## How it works ``` Open: Browser --POST inv/dopen {id | payload}--> server builds InvoiceDraftSession, caches it Browser --SignalR JoinDraft(token)--> joins the draft's group; spinner while loading Browser --POST inv/dstate {token}--> renders admin/new/req + server sums + validation Edit: Browser --POST inv/dpatch {token, delta}--> mutate + recompute + validate + version++ Server --SignalR draftReady{token,version}--> Browser re-fetches inv/dstate, re-renders Preview: Browser --POST inv/dpreview {token}--> PDF rendered straight from the cache (no upload) Save: Browser --POST inv/dsave {token}--> flush cache->DB (RegisterInvoiceAsync) + EventService toast History: Browser --POST inv/dhistory {token}--> change list -> "Änderungshistorie" dialog Discard: Browser --POST inv/ddiscard {token}--> reload session from DB draft -> draftReady Close: Browser --POST inv/dclose {token}--> session removed (+ LeaveDraft) Expiry: Server (timer) --SignalR draftExpiring{token,secondsLeft}--> warn "bitte zwischenspeichern" Server (evict) --SignalR draftClosed{token,reason}--> close the editor with a reason ``` - **Session** (`InvoiceDraftSession`) is a pure data holder: the editable payload as the exact editor JSON (`admin` / `new` / `req` blocks with `items`), plus server-computed `Sums`, `ValidationMessages`, `History`, `Version`, `Token`, `InvId`, `LastAccessUtc`. - **Calculation** (`InvoiceDraftCalculator`, static/pure) ports the former client math: `RecomputeItem` (quantity × price × VAT, the `quantChange` port), `RecomputeTotals` (the `invSumUpdate`/`csms` aggregation + §13b reverse-charge), `RecomputePositions` (numbers every line except heading/free-text lines continuously across the whole invoice — mirroring the editor's `invSumUpdate`, so the editor and the PDF show identical `Pos.` numbers, including after a reorder), and `Validate` (email/address/items/VAT-rate/negative-total checks). Being pure, it is exhaustively unit-tested. - **Sanitisation & reorder.** Scalar text deltas (`title`/`email`/`address`/`provisionperiod`/ `provisionlocation`) and the section heading (`block.replace`) are stripped of the editor's TinyMCE HTML (`
…
`, `