- Introduced comprehensive unit tests for the Fuchs_DataService library, covering DATEV header formatting, CSV/XML generation, and FdsMfrClient construction. - Implemented tests for FdsMfr.UpdateNeed parsing and FdsShared utility helpers, ensuring correct functionality and stability. - Added tests for FdsConfig and FdsMfrClient to validate configuration resolution and client construction. Document decisions on backend-authoritative invoice and reminder handling - Created ADR 0008 to clarify that all invoice types and reminder stages are backend-authoritative during drafting and previewing. - Established that all calculations and settings must be processed server-side, ensuring consistency between online editor and PDF outputs. Define irreversible mutations for set-price modes in invoices - Documented ADR 0009 to specify that the "Set mit Preis" and "Nur Set mit Preis" operations are irreversible mutations affecting service request blocks. - Clarified that these operations are not display toggles but actual data changes, ensuring clear expectations for invoice handling. Transition MFR ERP sync to in-process execution within the web app - Created ADR 0010 to outline the migration of Fuchs_DataService from a standalone service to an in-process library within the Fuchs web application. - Updated configuration and logging management to be handled by the host application, streamlining the sync process. Add publish profile and periodic hosted service for job scheduling - Introduced a publish profile for deployment to a specified folder. - Implemented PeriodicHostedService to manage multiple independent jobs, including the MFR ERP sync, with configurable execution intervals. Add dotnet-tools.json for EF Core CLI tools - Included dotnet-tools.json to manage the version of dotnet-ef for Entity Framework Core migrations and commands.
7.5 KiB
status, date, applyTo, supersededBy
| status | date | applyTo | supersededBy | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Accepted | 2026-07-15 |
|
0008 — Invoices and reminders (all kinds) are fully backend-authoritative; PDF and online editor must render identical content
Context
ADR 0006 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 inquantChange/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 fromdstate.sums($inv.d.footer);$inv.invSumUpdateno longer accumulates any of these — it only reassembles the row contract array needed to postreqto 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/ReminderDraftDeltatarget and reflected back throughdstate. 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 nextdstate/draftReadyrefresh. - Any text change (recipient email/address, invoice title, provision location/period,
section headings, item name/description/notes) is sanitised and stored server-side
(
InvoiceDraftEditService.HtmlToPlainet 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.setpricedelta /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'sreq/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'snotesarray 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.
- The online editor renders
- 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/dsaveflow. - 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 — seeFuchs.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.