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.
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
---
|
||||
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.
|
||||
Reference in New Issue
Block a user