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

115 lines
7.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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.