Files
Fuchs_Intranet/Fuchs/Docs/EVAL_live_invoice_editing.md
T
StefanandClaude Opus 4.8 af445c015e Add backend-authoritative invoice draft editing (ADR 0006/0007)
Move the invoice draft editor to a backend single source of truth: an
in-memory InvoiceDraftSession (per-token, cached) holds the editable payload,
server-computed sums/VAT and validation, plus an automatic change history. The
browser posts single edits; the server recomputes and signals the editing
session over a dedicated SignalR hub (DraftPreviewHub) to re-fetch.

This reverses the previously-documented stateless editor (EVAL_live_invoice_editing,
INVOICE_LIFECYCLE §10), by explicit product decision — captured in ADR 0006 and 0007
plus the live-draft-editing concept doc.

Backend (this milestone):
- InvoiceDraftSession + ChangeHistoryEntry data holders
- InvoiceDraftCalculator: pure port of quantChange/invSumUpdate (§13b, VAT-by-rate)
  and consistency checks — fully unit-tested
- IInvoiceDraftCache/InvoiceDraftCache: in-memory store with idle sliding TTL
- IInvoiceDraftService/InvoiceDraftEditService: open (payload or DB reload), patch,
  build state, flush via existing RegisterInvoiceAsync (no new persistence), preview
  from cache, discard (DB reload), history
- InvoiceDraftExpiryService: pre-expiry warning + eviction-with-reason
- DraftPreviewHub + IDraftNotifier/DraftNotifier: targeted draftReady/draftExpiring/
  draftClosed signals per draft token
- inv/dopen|dstate|dpatch|dpreview|dsave|dhistory|ddiscard|dclose endpoints; save
  reports success/failure via the existing EventService
- DI + hub mapping in Program.cs

Frontend (additive foundation): $fis.draft SignalR client for /draftpreview.
The editor DOM inversion (routing deltas, rendering from server state) is the
next, separately-verified step; existing endpoints are unaffected.

Tests: 30 new (calculator, cache, expiry, patch/history, flush); 306 total passing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 13:29:35 +02:00

111 lines
5.8 KiB
Markdown

# Evaluation — Backend-cached invoice editing over SignalR
> **⚠️ Superseded (2026-07-10).** This note's recommendation (keep the editor
> stateless; do **not** build the SignalR/server-cached model) was reversed by the
> product owner. Invoice draft editing is now backend-authoritative over an in-memory
> cache — see **ADR
> [`Decisions/0006-backend-authoritative-draft-editing.md`](Decisions/0006-backend-authoritative-draft-editing.md)**,
> [`Decisions/0007-targeted-draft-signalr-groups.md`](Decisions/0007-targeted-draft-signalr-groups.md)
> and the concept doc [`Concepts/live-draft-editing.md`](Concepts/live-draft-editing.md).
> The analysis below is retained for the historical rationale and the risks it flagged
> (server-held state, scaling/backplane, reconnect) — which the new design addresses or
> accepts explicitly as documented limitations.
**Idea (as proposed):** hold invoices that users are editing in a **server-side
cache**, keep a **SignalR / WebSocket** connection open, apply each front-end
change **in the backend**, and **push the recomputed state back** to the browser.
The backend becomes the single source of truth for the in-progress invoice.
This note evaluates that against the current design and recommends a path.
---
## 1. How invoice editing works today
- **Stateless.** The browser holds the editor state. It posts the whole `invc`
JSON to `req/sprep|sedit|save`; the server computes/persists and returns a PDF
preview (image collection). No per-user editing state lives on the server.
- Totals/§13b/§35a and now **set pricing** are computed from posted data; the
invoice **total comes from the registration balance**, not summed lines.
- Auth is cookie-based (`OCORECookieAuthenticationEvents`), SQL-first data access,
controller is stateless and DI-scoped.
**Implication:** the server is horizontally scalable and crash-tolerant for
editing — there is nothing to lose if a node restarts mid-edit.
---
## 2. What the proposal would buy
| Benefit | Real for Fuchs? |
|---|---|
| Server-authoritative calculation (one place for pricing/VAT/set rules) | **Partly already true** — the PDF/totals are server-computed; the editor only previews. The duplicated logic is the *live* line math in JS. |
| Real-time multi-user co-editing | **Low value** — invoices are edited by one back-office user at a time; concurrent editing of the same draft is rare. |
| Live validation / instant recompute without full round-trips | **Some value** — smoother UX than re-posting the whole `invc` for each tweak. |
| Reduced payload (deltas vs whole invoice) | Marginal — invoices are small. |
---
## 3. Costs and risks
- **Server-side edit state.** Per-user/per-draft cache with lifetime management
(idle expiry, explicit discard, max size), or the server leaks memory. Needs a
distributed cache if scaled out (Redis), or **sticky sessions** for SignalR.
- **Concurrency/locking.** Two tabs or two users on the same draft → need
optimistic concurrency / locking semantics that don't exist today.
- **Connection lifecycle.** Reconnect, replay, and "lost update" handling;
offline/flaky networks; auth on the socket (cookie works, but token refresh and
disconnect-on-logout must be handled).
- **Scaling.** SignalR with multiple instances needs a backplane (Redis/Azure
SignalR). Today the app has none.
- **Big rewrite of the editor.** The 1,200-line `fis.inv_shared.js` becomes an
event-driven client of server state — a substantial, risky rewrite of working
code, with new failure modes (desync between optimistic UI and server truth).
- **Testing surface** grows (connection states, races) far beyond the current
request/response model.
For a single-tenant back-office app with one editor at a time, this is a lot of
**accidental complexity** for modest UX gains.
---
## 4. Recommendation
**Do not do a full SignalR rewrite now.** It optimises a problem (real-time
collaboration, server-held edit state) the business doesn't strongly have, while
adding stateful-server, scaling, and reconnection complexity to an app that is
currently simple and robust.
Prefer an **incremental, lower-risk path** that captures most of the value:
1. **Single source of pricing truth (highest value, do first).**
Add a stateless endpoint `inv/calc` that takes the `invc` JSON and returns the
computed lines + totals + set-mode resolution using the **same backend code**
(`InvoiceSetPricing`, VAT, §13b/§35a). The editor calls it on change (debounced)
to recompute, instead of duplicating the math in JS. This removes the
front/back duplication — the actual pain — **without** sockets or server state.
It also makes the interplay fully unit-testable (the endpoint is pure).
2. **Keep preview as-is** (post → PDF image) but allow it to reuse `inv/calc`.
3. **If/when live UX is still wanted**, add SignalR **only as a transport** on top
of the stateless calc (push recompute results), keeping persistence stateless.
Defer server-held draft state until genuine multi-user co-editing is required.
4. **If server-held drafts are truly needed**, scope a pilot: one entity (invoice
draft), `IDistributedCache`-backed, explicit acquire/release lock, idle TTL,
and a reconnect/replay protocol — behind a feature flag, measured against the
current flow before rollout.
### Why this fits the codebase
It aligns with the just-completed DI service layer: the calc lives in
`IInvoiceService`/`InvoiceSetPricing` (already tested), reused by both the preview
and a future socket. We get "changes computed by the backend, reflected in the
frontend" — the stated goal — **without** turning a stateless, scalable web app
into a stateful real-time system prematurely.
**Suggested next step:** implement `inv/calc` (item 1) and migrate the editor's
line math to it; revisit SignalR only if real-time/co-editing becomes a real
requirement.