- 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.
123 lines
7.6 KiB
Markdown
123 lines
7.6 KiB
Markdown
---
|
|
status: Accepted
|
|
date: 2026-07-14
|
|
applyTo:
|
|
- "Fuchs/code/InvoiceSetPricing.cs"
|
|
- "Fuchs/Services/InvoiceDraft*"
|
|
- "Fuchs/Services/IInvoiceDraft*"
|
|
- "Fuchs/code/InvoiceDraftSession.cs"
|
|
- "Fuchs/code/InvoiceDraftCalculator.cs"
|
|
- "Fuchs/code/FuchsPdf.cs"
|
|
- "Fuchs/js/intranet/**"
|
|
- "Fuchs/Docs/INVOICE_SET_PRICING.md"
|
|
supersededBy: ""
|
|
---
|
|
|
|
# 0009 — The two menu set-price modes are per-service-request-block, irreversible cache mutations that insert a dedicated set row
|
|
|
|
## Context
|
|
ADRs [0006](0006-backend-authoritative-draft-editing.md) and
|
|
[0008](0008-invoices-and-reminders-fully-backend-authoritative.md) 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),
|
|
never `SetItmId`. Every block is treated as one set, whether or not it contains any
|
|
`type == "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:
|
|
|
|
1. **Inline set-item switch — unchanged.** The row context button on a single `type == "set"`
|
|
item (`$inv.toSetPrice` → `item.setprice` delta → `InvoiceDraftEditService.ApplyItemSetPrice`).
|
|
It is `SetItmId`-based, sums the header's members onto the header, sets the members' prices to
|
|
`null` (empty cell, excluded from the sum — consistent with modes 2 & 3, not `0`), and is
|
|
one-way. This is the **only** set-price operation that reads `SetItmId`. The button is shown —
|
|
and the operation available — **only** for items that are `type == "set"` **and** carry a
|
|
`SetItmId` (and are still unconverted, i.e. own price `0`); an item missing either condition
|
|
never offers it.
|
|
|
|
2. **"Set mit Preis" (menu) — per-block, irreversible mutation.** Applied server-side to the
|
|
cached `InvoiceDraftSession`, grouped by `ServiceRequestId`. 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`, **not** `0`) so the row renders with an **empty** price/total cell. `null` and `0`
|
|
are semantically distinct here: `null` means "no price — render an empty cell and exclude
|
|
from the block sum", whereas `0` would legitimately print `0,00 €`. The rows themselves are
|
|
retained.
|
|
|
|
3. **"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 mutating `InvoiceDraftDelta`, computed on the server (never in the browser).
|
|
There is no render-time `admin.setmode` grouping 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.
|
|
- **`SetItmId` is 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, `null`
|
|
counts as no contribution) or because they are gone (mode 3). So `InvoiceBalance`/
|
|
`InvoiceBalance_net` are 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, `InvoiceSetPricing` stops being a non-mutating render transform over
|
|
`type == "set"` groups; the grouping/insert/blank/remove is a real mutation in
|
|
`InvoiceDraftEditService`, keyed on the block. The inline `item.setprice` switch (operation 1)
|
|
remains the sole `SetItmId`-based, set-item-scoped operation.
|
|
- The previous `admin.setmode` display-flag model for these two modes — persisted `setmode:`
|
|
`InvoiceOptions` token, "Set-Preisanzeige menu entry disappears while unset", `Build(...)`
|
|
choosing `ShowPrice` per member at render time — is retired. Because the operation is a
|
|
one-shot irreversible mutation, there is no persisted display state to toggle. Any residual
|
|
`setmode:` 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** — `0` is
|
|
ambiguous (it prints `0,00 €`), so the frontend could not tell an empty cell from a genuine
|
|
zero price. Blanked members are set to `null` precisely 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.
|