Files
Fuchs_Intranet/Fuchs/Docs/Decisions/0019-set-price-modes-are-keyed-on-the-set.md
T
StefanandClaude Fable 5.1 5fa7a2b50f Key the set-price menu modes on the set instead of the service-request block (ADR 0019)
"Set mit Preis" / "Nur Set mit Preis" collapsed every block into an inserted
"Gesamtumfang pauschal" row, nulled/removed all lines and cleared the user-defined
set structure. Both modes now convert every still-unconverted top-level set:
the set line takes the sum of its subtree, its members render without a price
(setprice) or are removed from itm + items (setonly); lines outside a set stay
individually priced and several sets stay separate lines. Nested sets are priced
once via their top-level set. One conversion primitive (ConvertSetHeader) now
backs all three triggers; item.setprice is a no-op on an already-converted set
(a repeated delta used to reset the set price to 0). The header's price_net is
mirrored too (flat PDF path unit price), and Build treats a null total_net as
"no price" like the flat path. The editor checks for a set before offering the
modes and the menu entry stays available for sets added later.

ADR 0009 is superseded by 0019; INVOICE_SET_PRICING.md rewritten accordingly.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 22:19:01 +02:00

112 lines
7.0 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-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.