--- status: Accepted date: 2026-09-10 applyTo: - "Fuchs/code/InvoiceSetPricing.cs" - "Fuchs/code/InvoiceSetStructure.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: "" --- # 0019 — The two set-price menu modes are keyed on the set, not on the service-request block ## Context ADR [0009](0009-block-setprice-modes-are-irreversible-mutations.md) defined the two menu modes ("Set mit Preis" / "Nur Set mit Preis") as per-**service-request-block** conversions: every block was collapsed into one inserted "Gesamtumfang pauschal" set row carrying the block's whole sum, every original line of the block was price-nulled or removed, and any existing set membership was neutralised (`SetItmId`/`setId` cleared, existing set rows demoted). Grouping by `SetItmId` was explicitly rejected there. ADR [0016](0016-user-defined-set-structure.md) then made set membership an explicit, user-editable parent link with a dialog to define exactly which line belongs to which set, and let sets nest. With that in place, the block-wide collapse actively destroys the structure the user just defined: it discards their sets, prices lines that were never meant to be in a set, and can never show two sets of one service request as two lines. The product owner has therefore redefined the two modes again, this time onto the set structure: - Choosing **"Set mit Preis – Positionen ohne Preis"** must give **each set** one price, show that set's positions without a price, and leave every item that is in **no** set listed individually with its own price. Sets and single items together make up the invoice total. - Choosing **"Nur Set mit Preis – Positionen ausgeblendet"** must hide the positions belonging to the sets, while the sets themselves and the items outside any set stay visible. - **Several sets stay several separate lines** — they are never merged into one. This decision records that redefinition and **supersedes ADR 0009**. Function 1 (the inline set-item switch) is carried over from 0009 unchanged, as is ADR 0008's rule that every calculation happens server-side and the PDF renders exactly what the editor shows. ## Decision There are still three set-price operations, but they now share one grouping key — the set — and one conversion primitive: 1. **Inline set-item switch (`item.setprice`) — unchanged from ADR 0009**, except that it is now a thin wrapper over the shared primitive and is a **no-op on an already-converted set** (own price non-zero). Without that guard a repeated delta would re-sum the members the first conversion had already nulled and reset the set price to 0. 2. **"Set mit Preis" (`block.setprice`) — per set, irreversible.** For **every still-unconverted top-level set of every block**: sum the set's transitive descendants onto the set line and set each descendant's price fields to `null` (empty cell, excluded from the sum — never `0`, which would print a genuine `0,00 €`). The descendant rows are kept. 3. **"Nur Set mit Preis" (`block.setonly`) — per set, irreversible.** As above, and additionally **remove** the whole subtree of every top-level set (its members and any nested set line) from both line shapes (`itm` and `items`). A set that was already converted keeps its price and still has its members removed. Properties common to modes 2 and 3: - **The set is the grouping key.** No row is inserted, no row is demoted, no `SetItmId`/`setId` is cleared. The user-defined structure (ADR 0016) is what the modes act on and what survives them. - **Lines outside every set are untouched** — they keep their individual price and stay listed as ordinary positions. Sets and standalone lines together form the total. - **Only top-level sets are priced.** A nested set is part of its parent's subtree and is blanked (mode 2) or removed (mode 3) with it, so no value is ever counted twice. - **Empty sets and set-free invoices are no-ops** — no version bump, no history entry. The editor therefore checks the authoritative state for a set before opening the mode dialog and otherwise tells the user to define a set first ("Set-Struktur bearbeiten"). - **Repeatable, still one-way.** Running a mode again converts only sets that are still unconverted, so the menu entry stays available (the ADR 0009 `admin.setmode` gate on the entry is gone); no operation converts anything back. - **Mutation, not display.** The change is written into `InvoiceDraftSession.Req` server-side; there is no render-time grouping flag. A residual `setmode:` `InvoiceOptions` token from the pre-0009 display-mode model still degrades safely (ignored) and is not re-persisted. - **Total conserved, editor and PDF identical** (ADR 0008): a blanked member contributes nothing, a removed one is gone, and in both cases its value already sits on the set line. ## Consequences - `InvoiceDraftEditService.ApplyBlockSetPricing` (block aggregation, inserted `bset_*` row, `DemoteSet`, `setId` neutralisation) is gone; `ApplySetPricing` walks the set forest through `InvoiceSetStructure` and delegates the per-set work to `ConvertSetHeader`, which `ApplyItemSetPrice` now uses too. One conversion rule, three triggers. - Because membership is preserved, `InvoiceSetPricing.Build` (and `BuildSetDisplay`) governs the rendering of the result again: a converted set's members are price-blanked, non-set lines keep their own price. The PDF path needs no special case for the menu modes. - The "Gesamtumfang pauschal" label and the notion of a service request as an implicit set no longer exist. A user who wants one price for a whole service request creates a set over its lines in the structure dialog first. - Tests: `InvoiceDraftServiceTests` covers per-set pricing with a standalone item alongside, several sets kept separate, nested sets priced once, member removal in both line shapes, the already-converted set, the repeated `item.setprice` no-op, and the set-free/empty-set no-ops. ## Alternatives considered - **Keep the block collapse and let the user pick the block** — rejected: it still cannot express two sets in one service request, and it overwrites the structure ADR 0016 exists to let the user define. - **Price nested sets individually as well** — rejected: their value is already contained in the top-level set's price, so both being priced would double the invoice's printed sum. - **Fall back to the old block behaviour when an invoice contains no set** — rejected: it would make the same menu entry do two very different things. Without a set there is nothing to price as a set, so the modes are a no-op and the editor points the user at the structure dialog. - **Make the modes reversible display toggles again** — rejected (as in ADR 0009): the set price must be a real, hand-editable value in the document, and "Nur Set mit Preis" must actually drop the member lines.