--- status: Accepted date: 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/**" supersededBy: "" --- # 0006 — Invoice draft editing is backend-authoritative over an in-memory cache ## Context The invoice editor was deliberately **stateless**: the browser held the working model, computed totals/VAT client-side (`invSumUpdate` in `fis.inv_shared.js`) and re-posted the whole `invc` JSON on every preview/save. `EVAL_live_invoice_editing.md` (2026) recommended keeping it that way and **against** a server-cached, SignalR-driven model, because the real-time/co-editing benefits were weak for a single back-office editor. The product owner has since decided the trade-off differently and prioritised a **single source of truth in the backend** with server-computed sums, server-side plausibility/consistency checks, in-place PDF preview without re-upload, an automatic change history, and an explicit discard. This decision records that reversal and the architecture chosen to implement it. ## Decision While a user edits an invoice draft, the authoritative state lives **server-side** in an in-memory `InvoiceDraftSession` (`Fuchs/code/InvoiceDraftSession.cs`), held by the singleton `IInvoiceDraftCache` and orchestrated by the scoped `IInvoiceDraftService` (`InvoiceDraftEditService`). The browser is a pure view/input layer. - **Truth & calculation on the server.** `InvoiceDraftCalculator` is the pure, unit-tested port of the former client-side math (`quantChange` + `setVat` + `invSumUpdate`), including per-line net/VAT/service-value multiplication (`RecomputeLineValues`), the §13b reverse-charge rule and VAT-per-rate grouping. The browser performs **no arithmetic whatsoever** — not even a single line's `net = qty × price` — it only renders the server's `req`/`sums`. - **Commands are ordinary POSTs; signals are SignalR.** The editor posts single edits to `inv/dpatch` (and `dopen`/`dstate`/`dpreview`/`dsave`/`dhistory`/`ddiscard`/`dclose`). The server mutates the session, recomputes, validates, bumps a version, and pings the editing browser (`draftReady`) to re-fetch `inv/dstate`. See [0007](0007-targeted-draft-signalr-groups.md) for the targeted-signal transport. - **Cache-only until Zwischenspeichern/Finalise.** Opening builds the session (from a brand-new payload or by reloading a DB draft); edits touch only the cache. `dsave` flushes the session to the DB by reusing the existing `IInvoiceService.RegisterInvoiceAsync` — **no new persistence path** — and reports success/failure through the existing `IEventService` (ADR 0001). Finalise continues through `req/sconf`. - **Preview from cache.** `inv/dpreview` renders the draft PDF straight from the session (synthesised registration), with no client upload. - **Automatic change history.** Every applied patch appends a `ChangeHistoryEntry` (cache-only, never persisted); `inv/dhistory` exposes it for the "Änderungshistorie" dialog. - **Idle lifecycle with user warning.** `InvoiceDraftExpiryService` warns the editing browser before a session's idle TTL lapses (`draftExpiring`) and, on eviction, tells it to close the editor with a reason (`draftClosed`). TTL and warning lead are under `Fuchs:DraftEditing`. ## Consequences - The server is now **stateful for in-progress drafts**. This is acceptable for a single-instance deployment; **scale-out requires sticky sessions or a distributed cache/SignalR backplane** — none exist today, so this is a documented limitation, not a silent assumption. - New editor interactions must be modelled as a **delta** applied server-side (add a case in `InvoiceDraftEditService.ApplyDelta` + calculator handling), never as a new client-side calculation. Do not reintroduce client-side totals. - `FdsInvoiceData` stays a pure data holder; `InvoiceDraftSession` is likewise a data holder, with all logic in the service/calculator (mirrors the existing service split). - Reminders (Mahnungen) are intended to follow the identical pattern as a second phase; this decision covers invoices first (the pilot) and applies to the reminder mirror when built. - `EVAL_live_invoice_editing.md` and `INVOICE_LIFECYCLE.md` §4/§10 (the "stateless editor" invariant) are superseded by this decision for the draft-editing flow and have been annotated accordingly. ## Alternatives considered - **Keep the stateless editor** (the prior recommendation): rejected by the product owner in favour of a backend single source of truth. - **Full bidirectional SignalR hub for commands too**: rejected — edits as POSTs reuse the existing controller/auth pattern and avoid a command reconnect/replay protocol; the hub carries only coordination signals. - **Write-through to the DB on every edit**: rejected — conflicts with the "Zwischenspeichern = persist the cache" semantics and adds DB load; the cache is the truth until an explicit save/finalise.