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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
e53d8962ad
commit
af445c015e
@@ -0,0 +1,59 @@
|
||||
---
|
||||
status: Accepted
|
||||
date: 2026-07-10
|
||||
applyTo:
|
||||
- "Fuchs/Notifications/DraftPreviewHub.cs"
|
||||
- "Fuchs/Notifications/IDraftNotifier.cs"
|
||||
- "Fuchs/Notifications/DraftNotifier.cs"
|
||||
- "Fuchs/Program.cs"
|
||||
- "Fuchs/js/intranet/**"
|
||||
supersededBy: ""
|
||||
---
|
||||
|
||||
# 0007 — Draft-editing signals are targeted via a dedicated hub with per-draft groups
|
||||
|
||||
## Context
|
||||
Backend-authoritative draft editing (ADR 0006) needs to notify **exactly the one
|
||||
browser** editing a given draft that its cached state changed, is about to expire, or
|
||||
was closed. The existing `NotificationHub` (ADR 0002) deliberately **broadcasts** every
|
||||
business toast to all logged-in sessions and explicitly deferred per-user/targeted
|
||||
delivery as "a new decision". Draft coordination pings are high-frequency, per-editor,
|
||||
and must not spray to every session.
|
||||
|
||||
## Decision
|
||||
Draft signals use a **dedicated** SignalR hub, `DraftPreviewHub`, mapped at
|
||||
`/draftpreview` (separate from `NotificationHub` at `/notifications`). Targeting is by
|
||||
**SignalR group named after the draft's session token**:
|
||||
|
||||
- The client calls the hub methods `JoinDraft(token)` / `LeaveDraft(token)` to
|
||||
subscribe/unsubscribe its connection to a draft's group. The hub carries **no
|
||||
commands** — only group membership (edits are POSTs; see ADR 0006).
|
||||
- The server sends via `IDraftNotifier` (`DraftNotifier`) to `Clients.Group(token)`:
|
||||
`draftReady{token,version}` (re-fetch), `draftExpiring{token,secondsLeft}` (idle
|
||||
warning), `draftClosed{token,reason}` (session evicted/discarded → close the editor).
|
||||
- Like `EventService`, delivery failures are logged and swallowed — a missed
|
||||
coordination ping must never fail the underlying operation; the client also re-syncs on
|
||||
reconnect and on its next POST.
|
||||
|
||||
Business success/failure messages for draft operations (e.g. "Zwischenstand
|
||||
gespeichert") continue to flow through `IEventService`/`NotificationHub`, **not** this
|
||||
hub — the two channels stay separate.
|
||||
|
||||
## Consequences
|
||||
- The session **token doubles as the group name**; it is an opaque GUID and must not
|
||||
encode sensitive data. Any browser that knows a token can join its group, so tokens
|
||||
must be treated as capabilities and only handed to the authenticated editor that opened
|
||||
the draft.
|
||||
- Adding a new draft signal means adding a method to `IDraftNotifier` + `DraftNotifier`
|
||||
and a client handler in `$fis.draft` — not overloading the business notification path.
|
||||
- ADR 0002 is unchanged: `NotificationHub` stays broadcast-only for toasts. This hub is
|
||||
the answer to its "if per-user targeting becomes necessary, that is a new decision".
|
||||
- Multi-instance scale-out needs a SignalR backplane for group delivery — same limitation
|
||||
as ADR 0006.
|
||||
|
||||
## Alternatives considered
|
||||
- **Reuse `NotificationHub` with groups**: rejected — it would entangle broadcast toasts
|
||||
with targeted, high-frequency editing pings and force ADR 0002's broadcast contract to
|
||||
change. A separate hub keeps the concerns and their decisions independent.
|
||||
- **Per-user groups (by account id)**: rejected — a user may open two drafts/tabs;
|
||||
per-draft-token groups target the precise editor and naturally support that.
|
||||
Reference in New Issue
Block a user