98 lines
6.2 KiB
Markdown
98 lines
6.2 KiB
Markdown
---
|
||
status: Active
|
||
lastUpdated: 2026-07-10
|
||
applyTo:
|
||
- "Fuchs/Services/InvoiceDraft*"
|
||
- "Fuchs/Services/IInvoiceDraft*"
|
||
- "Fuchs/code/InvoiceDraftSession.cs"
|
||
- "Fuchs/code/InvoiceDraftCalculator.cs"
|
||
- "Fuchs/Notifications/DraftPreviewHub.cs"
|
||
- "Fuchs/Notifications/*DraftNotifier*"
|
||
- "Fuchs/Controllers/IntranetController.InvoiceDraft.cs"
|
||
- "Fuchs/js/intranet/**"
|
||
relatedDecisions:
|
||
- "0006-backend-authoritative-draft-editing.md"
|
||
- "0007-targeted-draft-signalr-groups.md"
|
||
---
|
||
|
||
# Live draft editing (backend-authoritative invoice previews)
|
||
|
||
## Summary
|
||
While a back-office user edits an invoice draft, the authoritative state is held in
|
||
server memory, not in the browser. The browser posts single edits, the server mutates
|
||
the cached record, recomputes totals/VAT and re-validates, then pushes a "state changed"
|
||
signal so the browser re-fetches and re-renders. This makes the backend the single source
|
||
of truth (server-computed sums, consistency checks, in-place PDF preview, change history,
|
||
explicit discard), reversing the earlier stateless editor. Invoices are the pilot;
|
||
reminders are intended to mirror the same design.
|
||
|
||
## How it works
|
||
|
||
```
|
||
Open: Browser --POST inv/dopen {id | payload}--> server builds InvoiceDraftSession, caches it
|
||
Browser --SignalR JoinDraft(token)--> joins the draft's group; spinner while loading
|
||
Browser --POST inv/dstate {token}--> renders admin/new/req + server sums + validation
|
||
|
||
Edit: Browser --POST inv/dpatch {token, delta}--> mutate + recompute + validate + version++
|
||
Server --SignalR draftReady{token,version}--> Browser re-fetches inv/dstate, re-renders
|
||
|
||
Preview: Browser --POST inv/dpreview {token}--> PDF rendered straight from the cache (no upload)
|
||
Save: Browser --POST inv/dsave {token}--> flush cache->DB (RegisterInvoiceAsync) + EventService toast
|
||
History: Browser --POST inv/dhistory {token}--> change list -> "Änderungshistorie" dialog
|
||
Discard: Browser --POST inv/ddiscard {token}--> reload session from DB draft -> draftReady
|
||
Close: Browser --POST inv/dclose {token}--> session removed (+ LeaveDraft)
|
||
|
||
Expiry: Server (timer) --SignalR draftExpiring{token,secondsLeft}--> warn "bitte zwischenspeichern"
|
||
Server (evict) --SignalR draftClosed{token,reason}--> close the editor with a reason
|
||
```
|
||
|
||
- **Session** (`InvoiceDraftSession`) is a pure data holder: the editable payload as the
|
||
exact editor JSON (`admin` / `new` / `req` blocks with `items`), plus server-computed
|
||
`Sums`, `ValidationMessages`, `History`, `Version`, `Token`, `InvId`, `LastAccessUtc`.
|
||
- **Calculation** (`InvoiceDraftCalculator`, static/pure) ports the former client math:
|
||
`RecomputeItem` (quantity × price × VAT, the `quantChange` port), `RecomputeTotals`
|
||
(the `invSumUpdate`/`csms` aggregation + §13b reverse-charge), `RecomputePositions`
|
||
(numbers every line except heading/free-text lines continuously across the whole invoice —
|
||
mirroring the editor's `invSumUpdate`, so the editor and the PDF show identical `Pos.` numbers,
|
||
including after a reorder),
|
||
and `Validate` (email/address/items/VAT-rate/negative-total checks). Being pure, it is
|
||
exhaustively unit-tested.
|
||
- **Sanitisation & reorder.** Scalar text deltas (`title`/`email`/`address`/`provisionperiod`/
|
||
`provisionlocation`) and the section heading (`block.replace`) are stripped of the editor's
|
||
TinyMCE HTML (`<p>…</p>`, `<br>`) to plain text in `ApplyDelta` (`HtmlToPlain`) — the backend
|
||
is the single source of truth, so no HTML reaches the DB, the PDF or a reloaded draft. Section
|
||
drags post a `block.order` delta (`["id",…]`) that reorders `Req`; positions are then
|
||
renumbered and pushed back via the view state (`applyState`/`applyPositions`). The change
|
||
history records the **changed field** (e.g. the new heading text), never the whole block JSON.
|
||
The PDF (`FuchsPdf`) renders a heading row per block (`FdsInvoiceData.InvoiceBlocks`) and shows
|
||
every position's price (set members are priced like standalone lines; only `setonly` collapses
|
||
them), so the PDF preview mirrors the online editor.
|
||
- **Orchestration** (`InvoiceDraftEditService`, scoped) opens sessions (from a fresh
|
||
payload or by reloading a DB draft via `fds__getInvoice`, reshaped like
|
||
`BuildInvoiceRequestList`), applies deltas (`ApplyDelta`), builds the view-state DTO,
|
||
flushes to the DB by reusing `IInvoiceService.RegisterInvoiceAsync` (no new persistence
|
||
path), renders previews from a synthesised registration, and discards by reloading.
|
||
- **Cache** (`InvoiceDraftCache`, singleton) stores sessions by token with an idle sliding
|
||
TTL; `InvoiceDraftExpiryService` (a `BackgroundService`) warns before, and evicts after,
|
||
the TTL. TTL/warn-lead are configurable under `Fuchs:DraftEditing`.
|
||
- **Signals** (`DraftPreviewHub` at `/draftpreview` + `IDraftNotifier`) are targeted at the
|
||
editing browser via a group named after the session token: `draftReady`, `draftExpiring`,
|
||
`draftClosed`. Business success/failure still flows through `IEventService`/`NotificationHub`.
|
||
- **Frontend** (`$fis.draft` in `fis_main.js`, editor in `fis.inv_shared.js`) opens/joins,
|
||
posts one delta per change, shows a loading state whenever awaiting a signal, and offers
|
||
"Änderungen verwerfen" and "Änderungshistorie" menu actions. It no longer computes totals.
|
||
|
||
## Key files
|
||
- `Fuchs/code/InvoiceDraftSession.cs` — session + `ChangeHistoryEntry` + `InvoiceDraftSums`.
|
||
- `Fuchs/code/InvoiceDraftCalculator.cs` — pure recompute + validate.
|
||
- `Fuchs/Services/InvoiceDraftCache.cs` / `IInvoiceDraftCache.cs` — in-memory store + TTL.
|
||
- `Fuchs/Services/InvoiceDraftEditService.cs` / `IInvoiceDraftService.cs` — orchestration + delta contract.
|
||
- `Fuchs/Services/InvoiceDraftExpiryService.cs` — idle warn/evict monitor.
|
||
- `Fuchs/Notifications/DraftPreviewHub.cs`, `DraftNotifier.cs`, `IDraftNotifier.cs` — targeted signals.
|
||
- `Fuchs/Controllers/IntranetController.InvoiceDraft.cs` — `inv/d*` endpoints.
|
||
- `Fuchs/js/intranet/fis_main.js`, `Fuchs/js/intranet/modules/fis.inv_shared.js` — client.
|
||
|
||
## Related decisions
|
||
- [0006 — Backend-authoritative draft editing](../Decisions/0006-backend-authoritative-draft-editing.md)
|
||
- [0007 — Targeted draft SignalR groups](../Decisions/0007-targeted-draft-signalr-groups.md)
|