# 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 (implemented in the invoice editor) Wired in `Fuchs/js/intranet/modules/fis.inv_shared.js` (bundled to `wwwroot/web/fis.inv.de.js` via gulp `min:js`): 1. **Mode** — a 3-way switch (`$inv.ssetmode`, menu entry `setm`, label `$ict.setm`) writes the choice onto `admin.setmode` (`setprice` | `itemprices` | `setonly`). The back-end `FdsInvoiceData.BuildInvoiceOptions` turns that into the `setmode:` token inside `@InvoiceOptions` (default `setprice` omitted), persisted by `fds__createInvoice_Details` and read back by `InvoiceSetPricing.ModeFromInvoiceOptions`. This rides the **same `admin` channel as `§13b`** (the posted payload is `{admin, req, sms, new}` — `inv` is not sent). 2. **Item shape** — `$inv.invSumUpdate` now posts each request block's `items[]` in the back-end contract shape via `$inv.itemToContract`: `{ id, type, title, desc, qty, price_net, total_net, vat }`. (Previously the editor only posted the legacy on-screen `itm`/`co` objects, which `FdsInvoiceData.InvoiceItems` does not read — so line items never reached the C# PDF. This change closes that gap for **all** invoices, not just sets.) 3. **Set flags** — `invSumUpdate` tags items as it builds `items[]`: an item with `type === 'set'` is a header (`id` = its set id); the **following items in the same block become its members** (`setId` = the header's id) until the next set header. `mfr__items` has a `Type='set'` header but **no explicit member link**, so this "header claims the following items in its block" rule is the convention — adjust in `invSumUpdate` if mfr later exposes a real grouping. ### Editor → backend field normalization (`$inv.invcPayload`) The editor's internal model keeps the long-standing key names, but the migrated C# `BuildInvoiceParams` reads different ones. At post time `$inv.invcPayload(d)` maps the working model onto the exact field names the back-end reads (non-destructively): `sms.ttn → new.total_net`, `sms.ttb → new.total_gross`, each `sms.vat` rate → `new.vat__net`, `new.invoicetitle → new.title`, `new.loc → new.provisionlocation`, `admin.paymentterms → new.paymentterm`, `admin.CustomerId → admin.customerid`. Both `req/save` and `req/sprep|sedit` post through it, so titles/balances/VAT now reach the backend correctly. VAT (rate **and** amount) is taken by the backend directly from the posted `sms.vat` map via `FdsInvoiceData.HighestVat` (highest rate wins) — `invcPayload` therefore emits no per-rate `vat_*` keys. **Back-end fixes applied alongside the wiring:** - Heading/free-text lines (`type` `text`/`title`) now render a **blank** price/total (`InvoiceSetPricing.IsNoPriceLine`), instead of `0,00 €`. - VAT rate detection no longer reads line items (the old `items is List` test failed on Newtonsoft `JArray` and pinned `@InvoiceVAT_1` to `19`); it now comes from `sms.vat`, so non-19 % rates are stored correctly. Single-rate procs still store only the highest rate. The editor's running **total stays the member sum in every mode**, matching the registration balance — switching modes is purely presentational. ### 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 and creation. `setmode` persists via `InvoiceOptions`; the finalised document is rendered once and stored as a file, so re-rendering from line items is not needed for correctness. Persisting the per-item `type`/`setId` flags (an SSDT + `fds__createInvoice_Details` change) is only required if a finalised invoice must be **re-generated** from stored items in a different mode later — not done here.