Invoice set pricing: 3 display modes (backend contract + PDF + tests)

Sets (mfr__items Type='set') can be shown three ways on the invoice:
- SetPrice (default): set line priced, member items shown without price
- ItemPrices: member items priced, set line as a heading without price
- SetOnly: only the set line (priced), members removed

- InvoiceSetPricing (new): the authoritative, unit-tested transformation
  (SetDisplayMode + Build) that both sides agree on; set price always equals the
  sum of members. Mode is read from InvoiceOptions ("setmode:<mode>").
- FuchsPdf.ApplyInvoice renders through it: lines flagged ShowPrice=false print
  blank price/total cells; set headers are emphasised. Invoices without sets are
  unchanged. Totals come from the registration balance, so modes are purely
  presentational and never change the sum.
- InvoiceSetPricingTests (+14): all three modes, set-price = member sum, header
  total fallback, no-set pass-through, option parsing.
- Docs/INVOICE_SET_PRICING.md documents the front-end contract (the editor sets
  the mode token + tags set header/member items); the back-end does the rest.

Front-end editor wiring is specified in the doc but intentionally not shipped
blind (cannot validate the running editor here).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-05 16:42:44 +02:00
parent 2c17171e77
commit c358fdbdb2
4 changed files with 406 additions and 13 deletions
+63
View File
@@ -0,0 +1,63 @@
# Invoice "Set" Pricing — Design & Front-/Back-end Contract
Customer requirement: items declared as a **set** in `[dbo].[mfr__items]`
(`[Type] = 'set'`) should normally be shown as a single **set price** on the
invoice instead of being broken up into their member items and summed.
Three display modes (switchable in the invoice editor):
| Mode | Set line | Member items | Use as |
|---|---|---|---|
| **SetPrice** (default) | shown **with price** | shown **without price** | the new default |
| **ItemPrices** | shown as a heading **without price** | shown **with price** | the previous behaviour |
| **SetOnly** | shown **with price** | **removed** | compact |
> **Totals are unaffected.** The invoice total is taken from the registration
> balance (`InvoiceBalance` / `InvoiceBalance_net`), not by summing the rendered
> lines, so switching modes is purely presentational. The set price always
> equals the sum of its members (computed as a fallback when the set header
> carries no own price).
## Back-end (implemented + unit-tested)
- `Fuchs/code/InvoiceSetPricing.cs` — the authoritative transformation:
`SetDisplayMode` + `Build(items, mode)` → ordered `InvoiceSetLine`s, each with
`ShowPrice` and `IsSetHeader`. `ModeFromInvoiceOptions(...)` reads the mode
from the invoice options. Fully covered by `Fuchs.Tests/InvoiceSetPricingTests.cs`.
- `FuchsPdf.ApplyInvoice` renders through `InvoiceSetPricing.Build`: lines with
`ShowPrice == false` render **blank** price/total cells (not `0,00 €`), and the
set header line is rendered emphasised. Invoices without sets pass through
unchanged.
## Front-end contract (to wire in the invoice editor)
The editor already knows the set structure (from `fds__prepInvoice`) and assembles
the `invc` JSON. To drive the modes it must, when sending the invoice:
1. **Mode** — add a token to `inv.InvoiceOptions` (CSV, alongside `§13b`):
- `setmode:setprice` (default — may be omitted), `setmode:itemprices`, or `setmode:setonly`.
A 3-way switch in the editor sets this token.
2. **Item flags** — in each service request's `items[]`:
- the **set header** item: `type: "set"`, `id: "<setId>"`, and `total_net` =
the set price (or `0` to let the back-end sum the members);
- each **member** item: `setId: "<setId>"` (matching the header's id).
- standalone items need no extra fields.
That is the entire contract — the back-end does the rest. The editor's running
**total stays the member sum in every mode**, matching the registration balance.
### Why the switch lives in the editor
Set grouping is only known where the request/item tree is rendered (front-end).
The back-end intentionally stays the single, tested authority for *how* a chosen
mode maps to printed lines, so the editor only needs to pick the mode and tag the
items — it does not re-implement the pricing rules.
## Persistence note
Draft/preview PDFs render straight from the posted `invc` JSON, so the contract
works end-to-end for previews immediately. For a **finalised** invoice to re-render
in a chosen mode later, the per-item `type`/`setId` flags (and the `setmode`
option) must be persisted with the stored invoice items
(`fds__createInvoice_Details` / `fds__invoice_items`). `setmode` already persists
via `InvoiceOptions`; persisting the per-item set tags is a small SSDT + create-proc
change to make once the editor wiring is confirmed.