--- status: Accepted date: 2026-07-15 applyTo: - "Fuchs/Services/InvoiceDraft*" - "Fuchs/Services/IInvoiceDraft*" - "Fuchs/Services/ReminderDraft*" - "Fuchs/Services/IReminderDraft*" - "Fuchs/code/InvoiceDraftSession.cs" - "Fuchs/code/InvoiceDraftCalculator.cs" - "Fuchs/code/InvoiceSetPricing.cs" - "Fuchs/code/ReminderDraftSession.cs" - "Fuchs/code/ReminderDraftCalculator.cs" - "Fuchs/code/FuchsPdf.cs" - "Fuchs/js/intranet/**" supersededBy: "" --- # 0008 — Invoices and reminders (all kinds) are fully backend-authoritative; PDF and online editor must render identical content ## Context ADR [0006](0006-backend-authoritative-draft-editing.md) established the backend-authoritative draft-editing model for invoices and noted reminders were "intended to follow the identical pattern as a second phase". Both are now implemented (`InvoiceDraftEditService` / `ReminderDraftEditService`). In practice, ambiguity kept resurfacing about *which* invoice/ reminder kinds this covers and *which* kinds of change qualify as "must be computed server-side": e.g. whether a purely presentational client-side re-render (set-price display toggle, item reordering, position renumbering) was allowed to keep any client-side math, and whether this applies uniformly to every invoice type (regular `r`, partial/Abschlag `i`, final `f`, storno `c`) and every reminder stage, not just the pilot "regular invoice" flow. This decision closes that ambiguity explicitly. ## Decision **Every invoice (all `InvoiceType` kinds: regular, partial/Abschlagsrechnung, final/ Schlussrechnung, Storno/credit) and every reminder (all reminder stages/Mahnstufen) is backend-authoritative while being drafted or previewed.** This generalises and makes explicit what ADR 0006 already implied for the pilot flow: - **Any calculation** (net/VAT/gross totals, per-rate VAT grouping, service-refund figures, §13b reverse-charge suppression, set-price sums, open-amount for reminders, position/line numbering) is performed exclusively by the server (`InvoiceDraftCalculator`, `ReminderDraftCalculator`, `InvoiceSetPricing`). The browser never sums, subtracts, or otherwise derives a monetary or positional value — it only displays server-computed values. This includes the single-line arithmetic that used to run in `quantChange`/`setVat` (`net_val = qty × price`, `vat_val = net_val × rate`, service-net/-VAT splits): those handlers now only post the raw, unmultiplied field the user typed (`qn`/`v`/`vat`) and the server (`InvoiceDraftCalculator.RecomputeLineValues`) computes every derived line value. Likewise the invoice footer (net/VAT-by-rate/gross), the per-block "isum" cell, and the service-refund note figures are rendered exclusively from `dstate.sums` (`$inv.d.footer`); `$inv.invSumUpdate` no longer accumulates any of these — it only reassembles the row contract array needed to post `req` to the server and (on first load) seeds the session. - **Any setting** (§13b flag, set-pricing display mode, payment terms, contact, custom values, …) is applied server-side via a named `InvoiceDraftDelta`/`ReminderDraftDelta` target and reflected back through `dstate`. The client never mutates its local model as the source of truth for a setting; it optimistically reflects the *request* but always re-renders from the next `dstate`/`draftReady` refresh. - **Any text change** (recipient email/address, invoice title, provision location/period, section headings, item name/description/notes) is sanitised and stored server-side (`InvoiceDraftEditService.HtmlToPlain` et al.); the server's stored value is the one that reaches the PDF and any reloaded draft. - **Any reordering** (drag-reorder of service-request blocks/sections, drag-reorder of item rows within a block) is committed as a `block.order` (or equivalent) delta; the server performs the actual reorder and renumbers positions (`InvoiceDraftCalculator.RecomputePositions`). The client's drag interaction is input only — the rendered order after a refresh is the server's order, not whatever the browser left in the DOM mid-drag. - **Irreversible one-way conversions** (e.g. "Auf Setpreis umstellen" — switching a set's member items from individual prices to a single set price) are likewise backend-only operations (`item.setprice` delta / `InvoiceDraftEditService.ApplyItemSetPrice`), never computed or applied in the browser. - **The PDF must render 100% the same information and content as the online editor at any given moment.** Both consume the identical authoritative session data: - The online editor renders `dstate`'s `req`/`sums`/`setDisplay`/`notes` — all server-computed. - The PDF preview (`inv/dpreview`, `rem/dpreview`) renders straight from the same cached session via a synthesised registration (`InvoiceDraftEditService.RenderPreview` / `ReminderDraftEditService`'s reminder equivalent) — **not** from a separate client upload or a re-derived model. - `FuchsPdf.BuildInvoiceNotes` (notice paragraphs) is called identically for both the editor's `notes` array and the PDF body, so intro/closing texts can never drift between the two renderings. - Any new editor-visible fact (a new total, a new flag, a new note) must be added to the shared session/service layer once, not duplicated as separate editor-only and PDF-only logic. - This applies for the full lifecycle while a document is a draft (open → edit → preview → Zwischenspeichern) up to finalise; a finalised, persisted invoice/reminder is immutable and is rendered straight from its stored DB data (no draft session involved) — that path already has no client-side math to begin with. ## Consequences - New invoice/reminder editor features must be modelled as a server-side delta + calculator change, exactly as ADR 0006 already requires; this decision removes any residual excuse to special-case a "just this one is presentational, do it in JS" shortcut for reordering, display-mode toggles, or one-way conversions. - Any PDF-only or editor-only special-casing found in review is a bug against this decision — the shared session/service must be extended so both renderers read the same value/flag. - Reminder "Mahnstufen" and every invoice type share this obligation; there is no partial/ Abschlagsrechnung, Schlussrechnung, or Storno exemption while such a document is still a draft going through the same `dopen`/`dpatch`/`dpreview`/`dsave` flow. - Test coverage for the cache/session layer (`InvoiceDraftEditService`, `ReminderDraftEditService`, `InvoiceDraftCalculator`, `InvoiceSetPricing`) must exercise every mutating operation (text edits, reordering, all three set-pricing display modes, the set-price conversion, multi-rate VAT sums, full recompute) against mock datasets, since this is now the single place all of these behaviours are guaranteed correct — see `Fuchs.Tests/InvoiceDraftServiceTests.cs`, `Fuchs.Tests/ReminderDraftServiceTests.cs`, `Fuchs.Tests/InvoiceDraftCalculatorTests.cs`, `Fuchs.Tests/InvoiceSetPricingTests.cs`. ## Alternatives considered - **Scope this only to the invoice pilot flow** (leave reminders/other invoice kinds ambiguous): rejected — the ambiguity itself was the problem being fixed; the underlying session/service code already treats all kinds uniformly, so documenting anything narrower would misrepresent the code. - **Allow "purely cosmetic" client-side math for reordering/display toggles**: rejected — history showed exactly this exception is where drift crept in (e.g. the set-price toggle originally computed sums in the browser before being moved server-side); no exception is granted.