Files
Fuchs_Intranet/Fuchs/Docs/Concepts/live-draft-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

5.0 KiB
Raw Blame History

status, lastUpdated, applyTo, relatedDecisions
status lastUpdated applyTo relatedDecisions
Active 2026-07-10
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/**
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), and Validate (email/address/items/VAT-rate/negative-total checks). Being pure, it is exhaustively unit-tested.
  • 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.csinv/d* endpoints.
  • Fuchs/js/intranet/fis_main.js, Fuchs/js/intranet/modules/fis.inv_shared.js — client.