Files
Fuchs_Intranet/Fuchs/Docs/Decisions/0008-invoices-and-reminders-fully-backend-authoritative.md
T
Stefan 49e3ed2673 Add unit tests for Fuchs_DataService and related components
- 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.
2026-07-16 13:34:23 +02:00

7.5 KiB
Raw Blame History

status, date, applyTo, supersededBy
status date applyTo supersededBy
Accepted 2026-07-15
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/**

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 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.