- 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.6 KiB
status, date, applyTo, supersededBy
| status | date | applyTo | supersededBy | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| Accepted | 2026-07-14 |
|
0009 — The two menu set-price modes are per-service-request-block, irreversible cache mutations that insert a dedicated set row
Context
ADRs 0006 and
0008 made draft editing
backend-authoritative. Under that model the set-price feature had three functions, of
which the two menu-driven ones ("Set mit Preis" / "Nur Set mit Preis") were framed as
whole-invoice display modes (SetDisplayMode.SetPrice/SetOnly): a non-mutating,
render-time transform (InvoiceSetPricing.Build) over explicit type == "set" header items
and their SetItmId members, persisted only as an admin.setmode flag. ADR 0008 calls them
"display-mode toggles" and treats them as presentational.
The product owner has redefined those two menu functions. They are not display toggles and they are not keyed on set-item membership:
- Their grouping is the service request (
ServiceRequestId= the editor's tbody block), neverSetItmId. Every block is treated as one set, whether or not it contains anytype == "set"item. - Applying a mode is an irreversible data change written hard into the cached draft dataset — not a reversible view flag. There is no toggle back; the user adjusts the result by hand afterwards.
This decision records that redefinition. It refines ADR 0008's characterisation of these two operations (from "non-mutating display toggle" to "mutating, one-way conversion"); ADR 0008's broader rule — every calculation server-side, and the PDF renders 100% the same content as the editor — remains fully in force and is not superseded.
Decision
There are three distinct set-price operations, kept clearly separated:
-
Inline set-item switch — unchanged. The row context button on a single
type == "set"item ($inv.toSetPrice→item.setpricedelta →InvoiceDraftEditService.ApplyItemSetPrice). It isSetItmId-based, sums the header's members onto the header, sets the members' prices tonull(empty cell, excluded from the sum — consistent with modes 2 & 3, not0), and is one-way. This is the only set-price operation that readsSetItmId. The button is shown — and the operation available — only for items that aretype == "set"and carry aSetItmId(and are still unconverted, i.e. own price0); an item missing either condition never offers it. -
"Set mit Preis" (menu) — per-block, irreversible mutation. Applied server-side to the cached
InvoiceDraftSession, grouped byServiceRequestId. For every service-request block:- Insert one dedicated, emphasised set row at the top of the block, carrying the block's aggregated value (net + VAT + service-net/-VAT splits) as its price. This row is a real, editable line item with its own id, so the user can manually change the set value afterwards as an ordinary item edit.
- Null out the price of every existing item row in the block (set the price fields to
null, not0) so the row renders with an empty price/total cell.nulland0are semantically distinct here:nullmeans "no price — render an empty cell and exclude from the block sum", whereas0would legitimately print0,00 €. The rows themselves are retained.
-
"Nur Set mit Preis" (menu) — per-block, irreversible mutation. As above, grouped by
ServiceRequestId. For every block:- Insert the same dedicated, emphasised set row carrying the block's aggregated value.
- Remove every existing item row in the block from the dataset entirely (the lines are gone, not merely hidden).
Properties common to the two menu modes (2 and 3):
- Mutation, not display. The change is written into
InvoiceDraftSession.Req(the "cache dataset") as a mutatingInvoiceDraftDelta, computed on the server (never in the browser). There is no render-timeadmin.setmodegrouping flag driving how lines are shown, and no reversible toggle. - Irreversible. There is no patch to undo it. The only ways back are discarding the draft (reloads the DB state) or hand-editing the resulting rows.
SetItmIdis irrelevant. Membership is the block, full stop.- Total unchanged. The inserted set row's value equals the sum of the block's original items,
which are then excluded from the sum — either because their price is
null(mode 2,nullcounts as no contribution) or because they are gone (mode 3). SoInvoiceBalance/InvoiceBalance_netare unaffected. - Editor and PDF render identically (ADR 0008): the dedicated set row is emphasised in both, and both read the same mutated session.
Consequences
- For the two menu modes,
InvoiceSetPricingstops being a non-mutating render transform overtype == "set"groups; the grouping/insert/blank/remove is a real mutation inInvoiceDraftEditService, keyed on the block. The inlineitem.setpriceswitch (operation 1) remains the soleSetItmId-based, set-item-scoped operation. - The previous
admin.setmodedisplay-flag model for these two modes — persistedsetmode:InvoiceOptionstoken, "Set-Preisanzeige menu entry disappears while unset",Build(...)choosingShowPriceper member at render time — is retired. Because the operation is a one-shot irreversible mutation, there is no persisted display state to toggle. Any residualsetmode:token must degrade safely (ignored) and is no longer (re-)persisted. - New/changed behaviour must be modelled as a server-side delta + calculator/service change and
covered by tests (
InvoiceDraftServiceTests,InvoiceSetPricingTests): for each menu mode, assert the inserted set row's value equals the block sum, the total is unchanged, mode 2 nulls-but-keeps member rows while mode 3 removes them, and an empty block is a no-op. - Reminders follow the identical pattern when/if the same feature is offered there (ADR 0006/0008 reminder mirror).
Alternatives considered
Each of the following was raised and explicitly decided against as part of accepting this decision — they are rejected choices, not open options to revisit without a superseding ADR:
- Keep them as non-mutating display toggles (the prior design): explicitly rejected by the product owner — the set price must be a real, hand-editable value baked into the document, and "Nur Set mit Preis" must actually drop the member lines, not just hide them.
- Zero the blanked members' prices instead of nulling them: explicitly rejected —
0is ambiguous (it prints0,00 €), so the frontend could not tell an empty cell from a genuine zero price. Blanked members are set tonullprecisely to make "no price" unambiguous. - Carry the set price on the existing block heading row instead of a dedicated row: explicitly rejected — a separate, individually-editable set row keeps the section-heading semantics intact and gives the user a concrete line to adjust afterwards.
- Group by
SetItmId/type == "set"headers like the inline switch: explicitly rejected — the menu modes present each service request as one set, independent of any mfr set-item; conflating the two groupings is exactly the ambiguity this decision removes.